diff --git a/Cargo.toml b/Cargo.toml
index 8ac0f15e32d5dfb8ef956542b21e03835c708858..09da5682a5e1ad04928a78c1ef03723d6fddedd4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -66,16 +66,6 @@ harness = false
 name = "commit"
 harness = false
 
-[[bench]]
-name = "field_operations"
-path = "benches/operations/field.rs"
-harness = false
-
-[[bench]]
-name = "curve_group_operations"
-path = "benches/operations/curve_group.rs"
-harness = false
-
 [[example]]
 name = "bench_commit"
 path = "examples/benches/commit.rs"
@@ -83,3 +73,13 @@ path = "examples/benches/commit.rs"
 [[example]]
 name = "bench_setup_size"
 path = "examples/benches/setup_size.rs"
+
+[[example]]
+name = "bench_field_operations"
+path = "examples/benches/operations/field.rs"
+harness = false
+
+[[example]]
+name = "bench_curve_group_operations"
+path = "examples/benches/operations/curve_group.rs"
+harness = false
diff --git a/benches/README.md b/benches/README.md
index 48ac922fb1c8064937ccca8d1dd4b87f88e148ab..cf3482aa84c2f6c0a1da92783dfbefb0ca8f8f2b 100644
--- a/benches/README.md
+++ b/benches/README.md
@@ -16,52 +16,30 @@ python scripts/plot/benches.py results.ndjson --bench setup
 
 ## atomic operations
 ```nushell
-cargo criterion --output-format verbose --message-format json --bench field_operations out> field.ndjson
-cargo criterion --output-format verbose --message-format json --bench curve_group_operations out> curve.ndjson
+cargo run --example bench_field_operations -- --nb-measurements 1000
+    | lines
+    | each { from nuon }
+    | to ndjson
+    | save --force field.ndjson
+cargo run --example bench_curve_group_operations -- --nb-measurements 1000
+    | lines
+    | each { from nuon }
+    | to ndjson
+    | save --force curve_group.ndjson
 ```
 ```nushell
 def read-atomic-ops [
-    --clean, --include: list<string> = [], --exclude: list<string> = []
+    --include: list<string> = [], --exclude: list<string> = []
 ]: list -> record {
     let raw = $in
-        | where reason == "benchmark-complete"
-        | select id mean.estimate
-        | sort-by id
-        | update id { parse "{op} on {curve}" }
-        | flatten --all
-        | rename --column { op: "group", curve: "species", mean_estimate: "measurement" }
-
-    let clean = if $clean {
-        $raw | update measurement {|it|
-            let species = $it.species
-            # FIXME: bug when no parentheses
-            let r = (
-                $raw
-                    | where group == 'random sampling' and species == $species
-                    | into record
-                    | get measurement
-            )
-            let l = (
-                $raw
-                    | where group == 'legendre' and species == $species
-                    | into record
-                    | get measurement
-            )
-            match $it.group {
-                "addition" | "multiplication" | "substraction" => ($it.measurement - 2 * $r),
-                "random sampling" => $it.measurement,
-                "sqrt" => ($it.measurement - $r - $l),
-                _ => ($it.measurement - $r),
-            }
-        }
-    } else {
-        $raw
-    }
+        | insert t {|it| $it.times |math avg}
+        | reject times
+        | rename --column { op: "group", curve: "species", t: "measurement" }
 
     let included = if $include != [] {
-        $clean | where group in $include
+        $raw | where group in $include
     } else {
-        $clean
+        $raw
     }
 
     $included
@@ -76,7 +54,7 @@ def read-atomic-ops [
 ```nushell
 python scripts/plot/multi_bar.py --title "simple field operations" -l "time (in ns)" (
     open field.ndjson
-        | read-atomic-ops --clean --exclude [ "exponentiation", "legendre", "inverse", "sqrt" ]
+        | read-atomic-ops --exclude [ "exponentiation", "legendre", "inverse", "sqrt" ]
         | to json
 )
 python scripts/plot/multi_bar.py --title "complex field operations" -l "time (in ns)" (
@@ -84,8 +62,15 @@ python scripts/plot/multi_bar.py --title "complex field operations" -l "time (in
         | read-atomic-ops --include [ "exponentiation", "legendre", "inverse", "sqrt" ]
         | to json
 )
-python scripts/plot/multi_bar.py --title "curve group operations" -l "time (in ns)" (
-    open curve.ndjson | read-atomic-ops | to json
+python scripts/plot/multi_bar.py --title "simple curve group operations" -l "time (in ns)" (
+    open curve_group.ndjson
+        | read-atomic-ops --exclude [ "random sampling", "scalar multiplication", "affine scalar multiplication" ]
+        | to json
+)
+python scripts/plot/multi_bar.py --title "complex curve group operations" -l "time (in ns)" (
+    open curve_group.ndjson
+        | read-atomic-ops --include [ "random sampling", "scalar multiplication", "affine scalar multiplication" ]
+        | to json
 )
 ```
 
diff --git a/benches/operations/curve_group.rs b/benches/operations/curve_group.rs
deleted file mode 100644
index f5374edbcd9094676a7132cac701b506fa2357c9..0000000000000000000000000000000000000000
--- a/benches/operations/curve_group.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-// see `benches/README.md`
-use std::time::Duration;
-
-use ark_ec::CurveGroup;
-use ark_ff::PrimeField;
-
-use criterion::{criterion_group, criterion_main, Criterion};
-
-fn random_sampling_template<G: CurveGroup>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("random sampling on {}", curve), |b| {
-        b.iter(|| {
-            let _ = G::rand(&mut rng);
-        });
-    });
-}
-
-fn group_template<F: PrimeField, G: CurveGroup<ScalarField = F>>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("addition on {}", curve), |b| {
-        b.iter(|| {
-            let g1 = G::rand(&mut rng);
-            let g2 = G::rand(&mut rng);
-            g1 + g2
-        });
-    });
-
-    c.bench_function(&format!("substraction on {}", curve), |b| {
-        b.iter(|| {
-            let g1 = G::rand(&mut rng);
-            let g2 = G::rand(&mut rng);
-            g1 - g2
-        });
-    });
-
-    c.bench_function(&format!("double on {}", curve), |b| {
-        b.iter(|| {
-            let g1 = G::rand(&mut rng);
-            g1.double()
-        });
-    });
-
-    c.bench_function(&format!("scalar multiplication on {}", curve), |b| {
-        b.iter(|| {
-            let g1 = G::rand(&mut rng);
-            let f1 = F::rand(&mut rng);
-            g1.mul(f1)
-        });
-    });
-}
-
-fn curve_group_template<F: PrimeField, G: CurveGroup<ScalarField = F>>(
-    c: &mut Criterion,
-    curve: &str,
-) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("into affine on {}", curve), |b| {
-        b.iter(|| {
-            let g1 = G::rand(&mut rng);
-            g1.into_affine()
-        });
-    });
-
-    c.bench_function(&format!("from affine on {}", curve), |b| {
-        b.iter(|| {
-            let g1_affine = G::rand(&mut rng).into_affine();
-            let _: G = g1_affine.into();
-        });
-    });
-
-    c.bench_function(&format!("affine addition on {}", curve), |b| {
-        b.iter(|| {
-            let g1_affine = G::rand(&mut rng).into_affine();
-            let g2_affine = G::rand(&mut rng).into_affine();
-            g1_affine + g2_affine
-        });
-    });
-
-    c.bench_function(&format!("affine scalar multiplication on {}", curve), |b| {
-        b.iter(|| {
-            let g1_affine = G::rand(&mut rng).into_affine();
-            let f1 = F::rand(&mut rng);
-            g1_affine * f1
-        });
-    });
-}
-
-macro_rules! bench {
-    ($c:ident, $b:ident, G1=$g:ident, name=$n:expr) => {
-        random_sampling_template::<$c::$g>($b, $n);
-        group_template::<$c::Fr, $c::$g>($b, $n);
-        curve_group_template::<$c::Fr, $c::$g>($b, $n);
-    };
-}
-
-fn bench(c: &mut Criterion) {
-    bench!(ark_bls12_381, c, G1 = G1Projective, name = "BLS12-381");
-    bench!(ark_bn254, c, G1 = G1Projective, name = "BN-254");
-    bench!(ark_pallas, c, G1 = Projective, name = "PALLAS");
-}
-
-criterion_group!(
-    name = benches;
-    config = Criterion::default()
-        .warm_up_time(Duration::from_secs_f32(3.0))
-        .sample_size(100);
-    targets = bench
-);
-criterion_main!(benches);
diff --git a/benches/operations/field.rs b/benches/operations/field.rs
deleted file mode 100644
index e0514608ec9a5c28a3fb6c27c739ce3dd32e11b2..0000000000000000000000000000000000000000
--- a/benches/operations/field.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-// see `benches/README.md`
-use std::time::Duration;
-
-use ark_ff::PrimeField;
-
-use criterion::{criterion_group, criterion_main, Criterion};
-
-fn random_sampling_template<F: PrimeField>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("random sampling on {}", curve), |b| {
-        b.iter(|| {
-            let _ = F::rand(&mut rng);
-        });
-    });
-}
-
-fn additive_group_template<F: PrimeField>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("addition on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            let f2 = F::rand(&mut rng);
-            f1 + f2
-        });
-    });
-
-    c.bench_function(&format!("substraction on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            let f2 = F::rand(&mut rng);
-            f1 - f2
-        });
-    });
-
-    c.bench_function(&format!("double on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            f1.double()
-        });
-    });
-
-    c.bench_function(&format!("multiplication on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            let f2 = F::rand(&mut rng);
-            f1 * f2
-        });
-    });
-}
-
-fn field_template<F: PrimeField>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("square on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            f1.square()
-        });
-    });
-
-    c.bench_function(&format!("inverse on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            f1.inverse()
-        });
-    });
-
-    c.bench_function(&format!("legendre on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            f1.legendre()
-        });
-    });
-
-    c.bench_function(&format!("sqrt on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            if f1.legendre().is_qr() {
-                let _ = f1.sqrt();
-            }
-        });
-    });
-}
-
-fn prime_field_template<F: PrimeField>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("exponentiation on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            f1.pow(F::MODULUS)
-        });
-    });
-}
-
-fn conversions_template<F: PrimeField>(c: &mut Criterion, curve: &str) {
-    let mut rng = ark_std::test_rng();
-
-    c.bench_function(&format!("bigint conversion on {}", curve), |b| {
-        b.iter(|| {
-            let f1 = F::rand(&mut rng);
-            f1.into_bigint()
-        });
-    });
-}
-
-macro_rules! bench {
-    ($c:ident, $b:ident, F=$f:ident, name=$n:expr) => {
-        random_sampling_template::<$c::$f>($b, $n);
-        additive_group_template::<$c::$f>($b, $n);
-        field_template::<$c::$f>($b, $n);
-        prime_field_template::<$c::$f>($b, $n);
-        conversions_template::<$c::$f>($b, $n);
-    };
-}
-
-fn bench(c: &mut Criterion) {
-    bench!(ark_bls12_381, c, F = Fr, name = "BLS12-381");
-    bench!(ark_bn254, c, F = Fr, name = "BN-254");
-    bench!(ark_pallas, c, F = Fr, name = "PALLAS");
-}
-
-criterion_group!(
-    name = benches;
-    config = Criterion::default()
-        .warm_up_time(Duration::from_secs_f32(3.0))
-        .sample_size(100);
-    targets = bench
-);
-criterion_main!(benches);
diff --git a/examples/benches/operations/curve_group.rs b/examples/benches/operations/curve_group.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d6d2771a9e420c61067e04909ff39be276d75e14
--- /dev/null
+++ b/examples/benches/operations/curve_group.rs
@@ -0,0 +1,138 @@
+// see `benches/README.md`
+use std::time::{Duration, Instant};
+
+use ark_ec::CurveGroup;
+use ark_ff::PrimeField;
+use clap::{command, Parser};
+use rand::RngCore;
+
+fn bench(b: &Bencher, op: &str, thing: fn(&mut dyn RngCore) -> Duration) {
+    let mut rng = ark_std::test_rng();
+
+    let mut times = vec![];
+    for i in 0..b.nb_measurements {
+        eprint!(
+            "{} on {} [{:>5}/{}]\r",
+            op,
+            b.name,
+            i + 1,
+            b.nb_measurements
+        );
+
+        times.push(thing(&mut rng).as_nanos());
+    }
+    eprintln!();
+
+    println!(
+        r#"{{op: "{}", curve: "{}", times: {:?}}}"#,
+        op, b.name, times
+    );
+}
+
+macro_rules! timeit {
+    ($f:tt) => {{
+        let start_time = Instant::now();
+        #[allow(clippy::redundant_closure_call)]
+        let _ = $f();
+        Instant::now().duration_since(start_time)
+    }};
+}
+
+#[derive(Clone)]
+struct Bencher {
+    nb_measurements: usize,
+    name: String,
+}
+
+impl Bencher {
+    fn new(nb_measurements: usize) -> Self {
+        Self {
+            nb_measurements,
+            name: "".to_string(),
+        }
+    }
+
+    fn with_name(&self, name: impl ToString) -> Self {
+        let mut new = self.clone();
+        new.name = name.to_string();
+        new
+    }
+}
+
+fn bench_template<F: PrimeField, G: CurveGroup<ScalarField = F>>(b: &Bencher) {
+    bench(b, "random sampling", |rng| timeit!((|| G::rand(rng))));
+
+    bench(b, "addition", |rng| {
+        let g1 = G::rand(rng);
+        let g2 = G::rand(rng);
+
+        timeit!((|| g1 + g2))
+    });
+
+    bench(b, "substraction", |rng| {
+        let g1 = G::rand(rng);
+        let g2 = G::rand(rng);
+
+        timeit!((|| g1 - g2))
+    });
+
+    bench(b, "double", |rng| {
+        let g1 = G::rand(rng);
+
+        timeit!((|| g1.double()))
+    });
+
+    bench(b, "scalar multiplication", |rng| {
+        let g1 = G::rand(rng);
+        let f1 = F::rand(rng);
+
+        timeit!((|| g1.mul(f1)))
+    });
+
+    bench(b, "into affine", |rng| {
+        let g1 = G::rand(rng);
+
+        timeit!((|| g1.into_affine()))
+    });
+
+    bench(b, "from affine", |rng| {
+        let g1_affine = G::rand(rng).into_affine();
+
+        timeit!((|| Into::<G>::into(g1_affine)))
+    });
+
+    bench(b, "affine addition", |rng| {
+        let g1_affine = G::rand(rng).into_affine();
+        let g2_affine = G::rand(rng).into_affine();
+
+        timeit!((|| g1_affine + g2_affine))
+    });
+
+    bench(b, "affine scalar multiplication", |rng| {
+        let g1_affine = G::rand(rng).into_affine();
+        let f1 = F::rand(rng);
+
+        timeit!((|| g1_affine * f1))
+    });
+}
+
+#[derive(Parser)]
+#[command(version, about, long_about = None)]
+struct Cli {
+    /// the number of measurements to repeat each case, larger values will reduce the variance of
+    /// the measurements
+    #[arg(short, long, default_value_t = 10)]
+    nb_measurements: usize,
+}
+
+fn main() {
+    let cli = Cli::parse();
+
+    let bencher = Bencher::new(cli.nb_measurements);
+
+    bench_template::<ark_bls12_381::Fr, ark_bls12_381::G1Projective>(
+        &bencher.with_name("BLS12-381"),
+    );
+    bench_template::<ark_bn254::Fr, ark_bn254::G1Projective>(&bencher.with_name("BN-254"));
+    bench_template::<ark_pallas::Fr, ark_pallas::Projective>(&bencher.with_name("PALLAS"));
+}
diff --git a/examples/benches/operations/field.rs b/examples/benches/operations/field.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5d4a8b6a1c12683edbc64dc00222080d626b3c12
--- /dev/null
+++ b/examples/benches/operations/field.rs
@@ -0,0 +1,148 @@
+// see `benches/README.md`
+use std::time::{Duration, Instant};
+
+use ark_ff::PrimeField;
+use clap::{arg, command, Parser};
+use rand::RngCore;
+
+fn bench(b: &Bencher, op: &str, thing: fn(&mut dyn RngCore) -> Duration) {
+    let mut rng = ark_std::test_rng();
+
+    let mut times = vec![];
+    for i in 0..b.nb_measurements {
+        eprint!(
+            "{} on {} [{:>5}/{}]\r",
+            op,
+            b.name,
+            i + 1,
+            b.nb_measurements
+        );
+
+        times.push(thing(&mut rng).as_nanos());
+    }
+    eprintln!();
+
+    println!(
+        r#"{{op: "{}", curve: "{}", times: {:?}}}"#,
+        op, b.name, times
+    );
+}
+
+macro_rules! timeit {
+    ($f:tt) => {{
+        let start_time = Instant::now();
+        #[allow(clippy::redundant_closure_call)]
+        let _ = $f();
+        Instant::now().duration_since(start_time)
+    }};
+}
+
+#[derive(Clone)]
+struct Bencher {
+    nb_measurements: usize,
+    name: String,
+}
+
+impl Bencher {
+    fn new(nb_measurements: usize) -> Self {
+        Self {
+            nb_measurements,
+            name: "".to_string(),
+        }
+    }
+
+    fn with_name(&self, name: impl ToString) -> Self {
+        let mut new = self.clone();
+        new.name = name.to_string();
+        new
+    }
+}
+
+fn bench_template<F: PrimeField>(b: &Bencher) {
+    bench(b, "random sampling", |rng| timeit!((|| F::rand(rng))));
+
+    bench(b, "addition", |rng| {
+        let f1 = F::rand(rng);
+        let f2 = F::rand(rng);
+
+        timeit!((|| f1 + f2))
+    });
+
+    bench(b, "substraction", |rng| {
+        let f1 = F::rand(rng);
+        let f2 = F::rand(rng);
+
+        timeit!((|| f1 - f2))
+    });
+
+    bench(b, "double", |rng| {
+        let f1 = F::rand(rng);
+
+        timeit!((|| f1.double()))
+    });
+
+    bench(b, "multiplication", |rng| {
+        let f1 = F::rand(rng);
+        let f2 = F::rand(rng);
+
+        timeit!((|| f1 * f2))
+    });
+
+    bench(b, "square", |rng| {
+        let f1 = F::rand(rng);
+
+        timeit!((|| f1.square()))
+    });
+
+    bench(b, "inverse", |rng| {
+        let f1 = F::rand(rng);
+
+        timeit!((|| f1.inverse()))
+    });
+
+    bench(b, "legendre", |rng| {
+        let f1 = F::rand(rng);
+
+        timeit!((|| f1.legendre()))
+    });
+
+    bench(b, "sqrt", |rng| {
+        let f1 = F::rand(rng);
+        if f1.legendre().is_qr() {
+            timeit!((|| f1.sqrt()))
+        } else {
+            Duration::default()
+        }
+    });
+
+    bench(b, "exponentiation", |rng| {
+        let f1 = F::rand(rng);
+
+        timeit!((|| f1.pow(F::MODULUS)))
+    });
+
+    bench(b, "into bigint", |rng| {
+        let f1 = F::rand(rng);
+
+        timeit!((|| f1.into_bigint()))
+    });
+}
+
+#[derive(Parser)]
+#[command(version, about, long_about = None)]
+struct Cli {
+    /// the number of measurements to repeat each case, larger values will reduce the variance of
+    /// the measurements
+    #[arg(short, long, default_value_t = 10)]
+    nb_measurements: usize,
+}
+
+fn main() {
+    let cli = Cli::parse();
+
+    let bencher = Bencher::new(cli.nb_measurements);
+
+    bench_template::<ark_bls12_381::Fr>(&bencher.with_name("BLS12-381"));
+    bench_template::<ark_bn254::Fr>(&bencher.with_name("BN-254"));
+    bench_template::<ark_pallas::Fr>(&bencher.with_name("PALLAS"));
+}