usearch 2.21.2 → 2.21.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,876 @@
1
+ //! Unified SimSIMD Benchmark Suite
2
+ //!
3
+ //! Compares SimSIMD vs native Rust implementations using Criterion.
4
+ //! Reports both performance and accuracy metrics.
5
+ //!
6
+ //! Run with:
7
+ //! ```bash
8
+ //! cargo bench --bench bench -- --quiet --noplot
9
+ //!
10
+ //! # Or with custom dimensions:
11
+ //! SIMSIMD_BENCH_DENSE_DIMENSIONS=2048 cargo bench --bench bench -- --quiet --noplot
12
+ //! ```
13
+ use std::hint::black_box;
14
+ use std::mem::size_of;
15
+ use std::time::Duration;
16
+
17
+ use criterion::{criterion_group, criterion_main, Criterion, Throughput};
18
+ use rand::rngs::StdRng;
19
+ use rand::Rng;
20
+ use rand::SeedableRng;
21
+
22
+ use simsimd::SpatialSimilarity as SimSIMD;
23
+
24
+ const BOLD: &str = "\x1b[1m";
25
+ const RESET: &str = "\x1b[0m";
26
+
27
+ fn get_dense_dimensions() -> usize {
28
+ std::env::var("SIMSIMD_BENCH_DENSE_DIMENSIONS")
29
+ .ok()
30
+ .and_then(|s| s.parse().ok())
31
+ .unwrap_or(1536)
32
+ }
33
+
34
+ #[allow(dead_code)]
35
+ fn get_curved_dimensions() -> usize {
36
+ std::env::var("SIMSIMD_BENCH_CURVED_DIMENSIONS")
37
+ .ok()
38
+ .and_then(|s| s.parse().ok())
39
+ .unwrap_or(8)
40
+ }
41
+
42
+ fn calculate_errors(baseline: &[f64], contender: &[f64]) -> (f64, f64) {
43
+ let mut mean_delta = 0.0f64;
44
+ let mut mean_relative_error = 0.0f64;
45
+
46
+ for (&b, &c) in baseline.iter().zip(contender.iter()) {
47
+ let abs_delta = (b - c).abs();
48
+ mean_delta += abs_delta;
49
+ if abs_delta != 0.0 && b != 0.0 {
50
+ mean_relative_error += abs_delta / b.abs();
51
+ }
52
+ }
53
+
54
+ mean_delta /= baseline.len() as f64;
55
+ mean_relative_error /= baseline.len() as f64;
56
+ (mean_delta, mean_relative_error)
57
+ }
58
+
59
+ fn print_error(abs_err: f64, rel_err: f64) {
60
+ println!(
61
+ " abs error: {}{:.6e}{}",
62
+ BOLD, abs_err, RESET
63
+ );
64
+ println!(
65
+ " rel error: {}{:.6e}{}",
66
+ BOLD, rel_err, RESET
67
+ );
68
+ }
69
+
70
+ // region: Accurate Baseline Implementations (f64)
71
+
72
+ fn accurate_cos(a: &[f32], b: &[f32]) -> f64 {
73
+ let mut dot = 0.0f64;
74
+ let mut norm_a = 0.0f64;
75
+ let mut norm_b = 0.0f64;
76
+ for i in 0..a.len() {
77
+ let (av, bv) = (a[i] as f64, b[i] as f64);
78
+ dot += av * bv;
79
+ norm_a += av * av;
80
+ norm_b += bv * bv;
81
+ }
82
+ 1.0 - dot / (norm_a * norm_b).sqrt()
83
+ }
84
+
85
+ fn accurate_l2sq(a: &[f32], b: &[f32]) -> f64 {
86
+ let mut sum = 0.0f64;
87
+ for i in 0..a.len() {
88
+ let diff = (a[i] - b[i]) as f64;
89
+ sum += diff * diff;
90
+ }
91
+ sum
92
+ }
93
+
94
+ fn accurate_dot(a: &[f32], b: &[f32]) -> f64 {
95
+ let mut sum = 0.0f64;
96
+ for i in 0..a.len() {
97
+ sum += (a[i] as f64) * (b[i] as f64);
98
+ }
99
+ -sum
100
+ }
101
+
102
+ fn accurate_l2(a: &[f32], b: &[f32]) -> f64 {
103
+ accurate_l2sq(a, b).sqrt()
104
+ }
105
+
106
+ fn accurate_i8_cos(a: &[i8], b: &[i8]) -> f64 {
107
+ let mut dot = 0i32;
108
+ let mut norm_a = 0i32;
109
+ let mut norm_b = 0i32;
110
+ for i in 0..a.len() {
111
+ let (av, bv) = (a[i] as i32, b[i] as i32);
112
+ dot += av * bv;
113
+ norm_a += av * av;
114
+ norm_b += bv * bv;
115
+ }
116
+ let dot_f = dot as f32;
117
+ let norm_a_f = norm_a as f32;
118
+ let norm_b_f = norm_b as f32;
119
+ (1.0 - dot_f / (norm_a_f * norm_b_f).sqrt()) as f64
120
+ }
121
+
122
+ fn accurate_i8_dot(a: &[i8], b: &[i8]) -> f64 {
123
+ let mut sum = 0i32;
124
+ for i in 0..a.len() {
125
+ sum += (a[i] as i32) * (b[i] as i32);
126
+ }
127
+ -(sum as f64)
128
+ }
129
+
130
+ fn accurate_i8_l2sq(a: &[i8], b: &[i8]) -> f64 {
131
+ let mut sum = 0i32;
132
+ for i in 0..a.len() {
133
+ let diff = (a[i] - b[i]) as i32;
134
+ sum += diff * diff;
135
+ }
136
+ sum as f64
137
+ }
138
+
139
+ fn accurate_i8_l2(a: &[i8], b: &[i8]) -> f64 {
140
+ accurate_i8_l2sq(a, b).sqrt()
141
+ }
142
+
143
+ // endregion: Accurate Baseline Implementations (f64)
144
+
145
+ // region: Baseline Implementations - Cosine Similarity
146
+
147
+ fn baseline_cos_procedural(a: &[f32], b: &[f32]) -> f64 {
148
+ let mut dot = 0.0f32;
149
+ let mut norm_a = 0.0f32;
150
+ let mut norm_b = 0.0f32;
151
+ for i in 0..a.len() {
152
+ let (av, bv) = (a[i], b[i]);
153
+ dot += av * bv;
154
+ norm_a += av * av;
155
+ norm_b += bv * bv;
156
+ }
157
+ (1.0 - dot / (norm_a * norm_b).sqrt()) as f64
158
+ }
159
+
160
+ fn baseline_cos_functional(a: &[f32], b: &[f32]) -> f64 {
161
+ let (dot, norm_a, norm_b) = a
162
+ .iter()
163
+ .zip(b)
164
+ .map(|(a, b)| (*a * *b, *a * *a, *b * *b))
165
+ .fold((0.0f32, 0.0f32, 0.0f32), |acc, x| {
166
+ (acc.0 + x.0, acc.1 + x.1, acc.2 + x.2)
167
+ });
168
+ (1.0 - (dot / (norm_a.sqrt() * norm_b.sqrt()))) as f64
169
+ }
170
+
171
+ fn baseline_cos_unrolled(a: &[f32], b: &[f32]) -> f64 {
172
+ let mut i = 0;
173
+ let remainder = a.len() % 8;
174
+ let mut acc = [0.0f32; 8];
175
+ let mut norm_a = [0.0f32; 8];
176
+ let mut norm_b = [0.0f32; 8];
177
+
178
+ unsafe {
179
+ while i < a.len() - remainder {
180
+ let [a1, a2, a3, a4, a5, a6, a7, a8] = [
181
+ *a.get_unchecked(i),
182
+ *a.get_unchecked(i + 1),
183
+ *a.get_unchecked(i + 2),
184
+ *a.get_unchecked(i + 3),
185
+ *a.get_unchecked(i + 4),
186
+ *a.get_unchecked(i + 5),
187
+ *a.get_unchecked(i + 6),
188
+ *a.get_unchecked(i + 7),
189
+ ];
190
+ let [b1, b2, b3, b4, b5, b6, b7, b8] = [
191
+ *b.get_unchecked(i),
192
+ *b.get_unchecked(i + 1),
193
+ *b.get_unchecked(i + 2),
194
+ *b.get_unchecked(i + 3),
195
+ *b.get_unchecked(i + 4),
196
+ *b.get_unchecked(i + 5),
197
+ *b.get_unchecked(i + 6),
198
+ *b.get_unchecked(i + 7),
199
+ ];
200
+
201
+ acc[0] += a1 * b1;
202
+ acc[1] += a2 * b2;
203
+ acc[2] += a3 * b3;
204
+ acc[3] += a4 * b4;
205
+ acc[4] += a5 * b5;
206
+ acc[5] += a6 * b6;
207
+ acc[6] += a7 * b7;
208
+ acc[7] += a8 * b8;
209
+ norm_a[0] += a1 * a1;
210
+ norm_a[1] += a2 * a2;
211
+ norm_a[2] += a3 * a3;
212
+ norm_a[3] += a4 * a4;
213
+ norm_a[4] += a5 * a5;
214
+ norm_a[5] += a6 * a6;
215
+ norm_a[6] += a7 * a7;
216
+ norm_a[7] += a8 * a8;
217
+ norm_b[0] += b1 * b1;
218
+ norm_b[1] += b2 * b2;
219
+ norm_b[2] += b3 * b3;
220
+ norm_b[3] += b4 * b4;
221
+ norm_b[4] += b5 * b5;
222
+ norm_b[5] += b6 * b6;
223
+ norm_b[6] += b7 * b7;
224
+ norm_b[7] += b8 * b8;
225
+ i += 8;
226
+ }
227
+ while i < a.len() {
228
+ let (av, bv) = (*a.get_unchecked(i), *b.get_unchecked(i));
229
+ acc[0] += av * bv;
230
+ norm_a[0] += av * av;
231
+ norm_b[0] += bv * bv;
232
+ i += 1;
233
+ }
234
+ }
235
+
236
+ let dot: f32 = acc.iter().sum();
237
+ let na: f32 = norm_a.iter().sum();
238
+ let nb: f32 = norm_b.iter().sum();
239
+ (1.0 - (dot / (na.sqrt() * nb.sqrt()))) as f64
240
+ }
241
+
242
+ // endregion: Baseline Implementations - Cosine Similarity
243
+
244
+ // region: Baseline Implementations - Squared Euclidean Distance
245
+
246
+ fn baseline_l2sq_procedural(a: &[f32], b: &[f32]) -> f64 {
247
+ let mut sum = 0.0f32;
248
+ for i in 0..a.len() {
249
+ let diff = a[i] - b[i];
250
+ sum += diff * diff;
251
+ }
252
+ sum as f64
253
+ }
254
+
255
+ fn baseline_l2sq_functional(a: &[f32], b: &[f32]) -> f64 {
256
+ a.iter()
257
+ .zip(b)
258
+ .map(|(x, y)| {
259
+ let diff = *x - *y;
260
+ diff * diff
261
+ })
262
+ .sum::<f32>() as f64
263
+ }
264
+
265
+ fn baseline_l2sq_unrolled(a: &[f32], b: &[f32]) -> f64 {
266
+ let mut i = 0;
267
+ let remainder = a.len() % 8;
268
+ let mut acc = [0.0f32; 8];
269
+
270
+ unsafe {
271
+ while i < a.len() - remainder {
272
+ let [a1, a2, a3, a4, a5, a6, a7, a8] = [
273
+ *a.get_unchecked(i),
274
+ *a.get_unchecked(i + 1),
275
+ *a.get_unchecked(i + 2),
276
+ *a.get_unchecked(i + 3),
277
+ *a.get_unchecked(i + 4),
278
+ *a.get_unchecked(i + 5),
279
+ *a.get_unchecked(i + 6),
280
+ *a.get_unchecked(i + 7),
281
+ ];
282
+ let [b1, b2, b3, b4, b5, b6, b7, b8] = [
283
+ *b.get_unchecked(i),
284
+ *b.get_unchecked(i + 1),
285
+ *b.get_unchecked(i + 2),
286
+ *b.get_unchecked(i + 3),
287
+ *b.get_unchecked(i + 4),
288
+ *b.get_unchecked(i + 5),
289
+ *b.get_unchecked(i + 6),
290
+ *b.get_unchecked(i + 7),
291
+ ];
292
+
293
+ let [d1, d2, d3, d4, d5, d6, d7, d8] = [
294
+ a1 - b1,
295
+ a2 - b2,
296
+ a3 - b3,
297
+ a4 - b4,
298
+ a5 - b5,
299
+ a6 - b6,
300
+ a7 - b7,
301
+ a8 - b8,
302
+ ];
303
+ acc[0] += d1 * d1;
304
+ acc[1] += d2 * d2;
305
+ acc[2] += d3 * d3;
306
+ acc[3] += d4 * d4;
307
+ acc[4] += d5 * d5;
308
+ acc[5] += d6 * d6;
309
+ acc[6] += d7 * d7;
310
+ acc[7] += d8 * d8;
311
+ i += 8;
312
+ }
313
+ while i < a.len() {
314
+ let diff = *a.get_unchecked(i) - *b.get_unchecked(i);
315
+ acc[0] += diff * diff;
316
+ i += 1;
317
+ }
318
+ }
319
+
320
+ acc.iter().sum::<f32>() as f64
321
+ }
322
+
323
+ // endregion: Baseline Implementations - Squared Euclidean Distance
324
+
325
+ // region: Baseline Implementations - Dot Product
326
+
327
+ fn baseline_dot_procedural(a: &[f32], b: &[f32]) -> f64 {
328
+ let mut sum = 0.0f32;
329
+ for i in 0..a.len() {
330
+ sum += a[i] * b[i];
331
+ }
332
+ -(sum as f64)
333
+ }
334
+
335
+ fn baseline_dot_functional(a: &[f32], b: &[f32]) -> f64 {
336
+ -(a.iter().zip(b).map(|(x, y)| *x * *y).sum::<f32>() as f64)
337
+ }
338
+
339
+ fn baseline_dot_unrolled(a: &[f32], b: &[f32]) -> f64 {
340
+ let mut i = 0;
341
+ let remainder = a.len() % 8;
342
+ let mut acc = [0.0f32; 8];
343
+
344
+ unsafe {
345
+ while i < a.len() - remainder {
346
+ let [a1, a2, a3, a4, a5, a6, a7, a8] = [
347
+ *a.get_unchecked(i),
348
+ *a.get_unchecked(i + 1),
349
+ *a.get_unchecked(i + 2),
350
+ *a.get_unchecked(i + 3),
351
+ *a.get_unchecked(i + 4),
352
+ *a.get_unchecked(i + 5),
353
+ *a.get_unchecked(i + 6),
354
+ *a.get_unchecked(i + 7),
355
+ ];
356
+ let [b1, b2, b3, b4, b5, b6, b7, b8] = [
357
+ *b.get_unchecked(i),
358
+ *b.get_unchecked(i + 1),
359
+ *b.get_unchecked(i + 2),
360
+ *b.get_unchecked(i + 3),
361
+ *b.get_unchecked(i + 4),
362
+ *b.get_unchecked(i + 5),
363
+ *b.get_unchecked(i + 6),
364
+ *b.get_unchecked(i + 7),
365
+ ];
366
+
367
+ acc[0] += a1 * b1;
368
+ acc[1] += a2 * b2;
369
+ acc[2] += a3 * b3;
370
+ acc[3] += a4 * b4;
371
+ acc[4] += a5 * b5;
372
+ acc[5] += a6 * b6;
373
+ acc[6] += a7 * b7;
374
+ acc[7] += a8 * b8;
375
+ i += 8;
376
+ }
377
+ while i < a.len() {
378
+ acc[0] += *a.get_unchecked(i) * *b.get_unchecked(i);
379
+ i += 1;
380
+ }
381
+ }
382
+
383
+ -(acc.iter().sum::<f32>() as f64)
384
+ }
385
+
386
+ // endregion: Baseline Implementations - Dot Product
387
+
388
+ // region: Baseline Implementations - Euclidean Distance
389
+
390
+ fn baseline_l2_procedural(a: &[f32], b: &[f32]) -> f64 {
391
+ baseline_l2sq_procedural(a, b).sqrt()
392
+ }
393
+
394
+ fn baseline_l2_functional(a: &[f32], b: &[f32]) -> f64 {
395
+ baseline_l2sq_functional(a, b).sqrt()
396
+ }
397
+
398
+ fn baseline_l2_unrolled(a: &[f32], b: &[f32]) -> f64 {
399
+ baseline_l2sq_unrolled(a, b).sqrt()
400
+ }
401
+
402
+ // endregion: Baseline Implementations - Euclidean Distance
403
+
404
+ // region: Baseline Implementations - i8 (i8 -> f64)
405
+
406
+ fn baseline_i8_cos_procedural(a: &[i8], b: &[i8]) -> f64 {
407
+ let mut dot = 0i32;
408
+ let mut norm_a = 0i32;
409
+ let mut norm_b = 0i32;
410
+ for i in 0..a.len() {
411
+ let (av, bv) = (a[i] as i32, b[i] as i32);
412
+ dot += av * bv;
413
+ norm_a += av * av;
414
+ norm_b += bv * bv;
415
+ }
416
+ let dot_f = dot as f32;
417
+ let norm_a_f = norm_a as f32;
418
+ let norm_b_f = norm_b as f32;
419
+ (1.0 - dot_f / (norm_a_f * norm_b_f).sqrt()) as f64
420
+ }
421
+
422
+ fn baseline_i8_dot_procedural(a: &[i8], b: &[i8]) -> f64 {
423
+ let mut sum = 0i32;
424
+ for i in 0..a.len() {
425
+ sum += (a[i] as i32) * (b[i] as i32);
426
+ }
427
+ -(sum as f64)
428
+ }
429
+
430
+ fn baseline_i8_l2sq_procedural(a: &[i8], b: &[i8]) -> f64 {
431
+ let mut sum = 0i32;
432
+ for i in 0..a.len() {
433
+ let diff = (a[i] - b[i]) as i32;
434
+ sum += diff * diff;
435
+ }
436
+ sum as f64
437
+ }
438
+
439
+ fn baseline_i8_l2_procedural(a: &[i8], b: &[i8]) -> f64 {
440
+ baseline_i8_l2sq_procedural(a, b).sqrt()
441
+ }
442
+
443
+ // endregion: Baseline Implementations - i8 (i8 -> f64)
444
+
445
+ // region: Benchmark Functions
446
+
447
+ pub fn f32_cosine_benchmark(c: &mut Criterion) {
448
+ let dimensions = get_dense_dimensions();
449
+ let bench_pairs_count = (128 * 1024 * 1024) / (dimensions * size_of::<f32>() * 2);
450
+ let bench_pairs: Vec<_> = (0..bench_pairs_count)
451
+ .map(|i| {
452
+ let mut rng = StdRng::seed_from_u64(1000 + i as u64);
453
+ let a: Vec<f32> = (0..dimensions).map(|_| rng.random()).collect();
454
+ let b: Vec<f32> = (0..dimensions).map(|_| rng.random()).collect();
455
+ (a, b)
456
+ })
457
+ .collect();
458
+
459
+ let accurate: Vec<f64> = bench_pairs
460
+ .iter()
461
+ .map(|(a, b)| accurate_cos(a, b))
462
+ .collect();
463
+ let procedural: Vec<f64> = bench_pairs
464
+ .iter()
465
+ .map(|(a, b)| baseline_cos_procedural(a, b))
466
+ .collect();
467
+ let functional: Vec<f64> = bench_pairs
468
+ .iter()
469
+ .map(|(a, b)| baseline_cos_functional(a, b))
470
+ .collect();
471
+ let unrolled: Vec<f64> = bench_pairs
472
+ .iter()
473
+ .map(|(a, b)| baseline_cos_unrolled(a, b))
474
+ .collect();
475
+ let simsimd: Vec<f64> = bench_pairs
476
+ .iter()
477
+ .map(|(a, b)| SimSIMD::cosine(a, b).unwrap())
478
+ .collect();
479
+
480
+ let simsimd_err = calculate_errors(&accurate, &simsimd);
481
+ let procedural_err = calculate_errors(&accurate, &procedural);
482
+ let functional_err = calculate_errors(&accurate, &functional);
483
+ let unrolled_err = calculate_errors(&accurate, &unrolled);
484
+
485
+ let mut group = c.benchmark_group(format!("f32/{}d/cosine", dimensions));
486
+ let bytes_per_op = (2 * dimensions * size_of::<f32>() * bench_pairs_count) as u64;
487
+ group.throughput(Throughput::Bytes(bytes_per_op));
488
+ group
489
+ .sample_size(50)
490
+ .measurement_time(Duration::from_secs(2))
491
+ .noise_threshold(0.05);
492
+
493
+ group.bench_function("simsimd", |b| {
494
+ b.iter(|| {
495
+ for (a, b) in &bench_pairs {
496
+ black_box(SimSIMD::cosine(a, b));
497
+ }
498
+ })
499
+ });
500
+ print_error(simsimd_err.0, simsimd_err.1);
501
+
502
+ group.bench_function("procedural", |b| {
503
+ b.iter(|| {
504
+ for (a, b) in &bench_pairs {
505
+ black_box(baseline_cos_procedural(a, b));
506
+ }
507
+ })
508
+ });
509
+ print_error(procedural_err.0, procedural_err.1);
510
+
511
+ group.bench_function("functional", |b| {
512
+ b.iter(|| {
513
+ for (a, b) in &bench_pairs {
514
+ black_box(baseline_cos_functional(a, b));
515
+ }
516
+ })
517
+ });
518
+ print_error(functional_err.0, functional_err.1);
519
+
520
+ group.bench_function("unrolled", |b| {
521
+ b.iter(|| {
522
+ for (a, b) in &bench_pairs {
523
+ black_box(baseline_cos_unrolled(a, b));
524
+ }
525
+ })
526
+ });
527
+ print_error(unrolled_err.0, unrolled_err.1);
528
+ group.finish();
529
+ }
530
+
531
+ pub fn f32_dot_benchmark(c: &mut Criterion) {
532
+ let dimensions = get_dense_dimensions();
533
+ let bench_pairs_count = (128 * 1024 * 1024) / (dimensions * size_of::<f32>() * 2);
534
+ let bench_pairs: Vec<_> = (0..bench_pairs_count)
535
+ .map(|i| {
536
+ let mut rng = StdRng::seed_from_u64(1000 + i as u64);
537
+ let a: Vec<f32> = (0..dimensions).map(|_| rng.random()).collect();
538
+ let b: Vec<f32> = (0..dimensions).map(|_| rng.random()).collect();
539
+ (a, b)
540
+ })
541
+ .collect();
542
+
543
+ let accurate: Vec<f64> = bench_pairs
544
+ .iter()
545
+ .map(|(a, b)| accurate_dot(a, b))
546
+ .collect();
547
+ let procedural: Vec<f64> = bench_pairs
548
+ .iter()
549
+ .map(|(a, b)| baseline_dot_procedural(a, b))
550
+ .collect();
551
+ let functional: Vec<f64> = bench_pairs
552
+ .iter()
553
+ .map(|(a, b)| baseline_dot_functional(a, b))
554
+ .collect();
555
+ let unrolled: Vec<f64> = bench_pairs
556
+ .iter()
557
+ .map(|(a, b)| baseline_dot_unrolled(a, b))
558
+ .collect();
559
+ let simsimd: Vec<f64> = bench_pairs
560
+ .iter()
561
+ .map(|(a, b)| SimSIMD::dot(a, b).unwrap())
562
+ .collect();
563
+
564
+ let simsimd_err = calculate_errors(&accurate, &simsimd);
565
+ let procedural_err = calculate_errors(&accurate, &procedural);
566
+ let functional_err = calculate_errors(&accurate, &functional);
567
+ let unrolled_err = calculate_errors(&accurate, &unrolled);
568
+
569
+ let mut group = c.benchmark_group(format!("f32/{}d/dot", dimensions));
570
+ let bytes_per_op = (2 * dimensions * size_of::<f32>() * bench_pairs_count) as u64;
571
+ group.throughput(Throughput::Bytes(bytes_per_op));
572
+ group
573
+ .sample_size(50)
574
+ .measurement_time(Duration::from_secs(2))
575
+ .noise_threshold(0.05);
576
+
577
+ group.bench_function("simsimd", |b| {
578
+ b.iter(|| {
579
+ for (a, b) in &bench_pairs {
580
+ black_box(SimSIMD::dot(a, b));
581
+ }
582
+ })
583
+ });
584
+ print_error(simsimd_err.0, simsimd_err.1);
585
+
586
+ group.bench_function("procedural", |b| {
587
+ b.iter(|| {
588
+ for (a, b) in &bench_pairs {
589
+ black_box(baseline_dot_procedural(a, b));
590
+ }
591
+ })
592
+ });
593
+ print_error(procedural_err.0, procedural_err.1);
594
+
595
+ group.bench_function("functional", |b| {
596
+ b.iter(|| {
597
+ for (a, b) in &bench_pairs {
598
+ black_box(baseline_dot_functional(a, b));
599
+ }
600
+ })
601
+ });
602
+ print_error(functional_err.0, functional_err.1);
603
+
604
+ group.bench_function("unrolled", |b| {
605
+ b.iter(|| {
606
+ for (a, b) in &bench_pairs {
607
+ black_box(baseline_dot_unrolled(a, b));
608
+ }
609
+ })
610
+ });
611
+ print_error(unrolled_err.0, unrolled_err.1);
612
+ group.finish();
613
+ }
614
+
615
+ pub fn f32_l2_benchmark(c: &mut Criterion) {
616
+ let dimensions = get_dense_dimensions();
617
+ let bench_pairs_count = (128 * 1024 * 1024) / (dimensions * size_of::<f32>() * 2);
618
+ let bench_pairs: Vec<_> = (0..bench_pairs_count)
619
+ .map(|i| {
620
+ let mut rng = StdRng::seed_from_u64(1000 + i as u64);
621
+ let a: Vec<f32> = (0..dimensions).map(|_| rng.random()).collect();
622
+ let b: Vec<f32> = (0..dimensions).map(|_| rng.random()).collect();
623
+ (a, b)
624
+ })
625
+ .collect();
626
+
627
+ let accurate: Vec<f64> = bench_pairs.iter().map(|(a, b)| accurate_l2(a, b)).collect();
628
+ let procedural: Vec<f64> = bench_pairs
629
+ .iter()
630
+ .map(|(a, b)| baseline_l2_procedural(a, b))
631
+ .collect();
632
+ let functional: Vec<f64> = bench_pairs
633
+ .iter()
634
+ .map(|(a, b)| baseline_l2_functional(a, b))
635
+ .collect();
636
+ let unrolled: Vec<f64> = bench_pairs
637
+ .iter()
638
+ .map(|(a, b)| baseline_l2_unrolled(a, b))
639
+ .collect();
640
+ let simsimd: Vec<f64> = bench_pairs
641
+ .iter()
642
+ .map(|(a, b)| SimSIMD::euclidean(a, b).unwrap())
643
+ .collect();
644
+
645
+ let simsimd_err = calculate_errors(&accurate, &simsimd);
646
+ let procedural_err = calculate_errors(&accurate, &procedural);
647
+ let functional_err = calculate_errors(&accurate, &functional);
648
+ let unrolled_err = calculate_errors(&accurate, &unrolled);
649
+
650
+ let mut group = c.benchmark_group(format!("f32/{}d/l2", dimensions));
651
+ let bytes_per_op = (2 * dimensions * size_of::<f32>() * bench_pairs_count) as u64;
652
+ group.throughput(Throughput::Bytes(bytes_per_op));
653
+ group
654
+ .sample_size(50)
655
+ .measurement_time(Duration::from_secs(2))
656
+ .noise_threshold(0.05);
657
+
658
+ group.bench_function("simsimd", |b| {
659
+ b.iter(|| {
660
+ for (a, b) in &bench_pairs {
661
+ black_box(SimSIMD::euclidean(a, b));
662
+ }
663
+ })
664
+ });
665
+ print_error(simsimd_err.0, simsimd_err.1);
666
+
667
+ group.bench_function("procedural", |b| {
668
+ b.iter(|| {
669
+ for (a, b) in &bench_pairs {
670
+ black_box(baseline_l2_procedural(a, b));
671
+ }
672
+ })
673
+ });
674
+ print_error(procedural_err.0, procedural_err.1);
675
+
676
+ group.bench_function("functional", |b| {
677
+ b.iter(|| {
678
+ for (a, b) in &bench_pairs {
679
+ black_box(baseline_l2_functional(a, b));
680
+ }
681
+ })
682
+ });
683
+ print_error(functional_err.0, functional_err.1);
684
+
685
+ group.bench_function("unrolled", |b| {
686
+ b.iter(|| {
687
+ for (a, b) in &bench_pairs {
688
+ black_box(baseline_l2_unrolled(a, b));
689
+ }
690
+ })
691
+ });
692
+ print_error(unrolled_err.0, unrolled_err.1);
693
+ group.finish();
694
+ }
695
+
696
+ pub fn i8_cosine_benchmark(c: &mut Criterion) {
697
+ let dimensions = get_dense_dimensions();
698
+ let bench_pairs_count = (128 * 1024 * 1024) / (dimensions * size_of::<i8>() * 2);
699
+ let bench_pairs: Vec<_> = (0..bench_pairs_count)
700
+ .map(|i| {
701
+ let mut rng = StdRng::seed_from_u64(1000 + i as u64);
702
+ let a: Vec<i8> = (0..dimensions).map(|_| rng.random()).collect();
703
+ let b: Vec<i8> = (0..dimensions).map(|_| rng.random()).collect();
704
+ (a, b)
705
+ })
706
+ .collect();
707
+
708
+ let accurate: Vec<f64> = bench_pairs
709
+ .iter()
710
+ .map(|(a, b)| accurate_i8_cos(a, b))
711
+ .collect();
712
+ let procedural: Vec<f64> = bench_pairs
713
+ .iter()
714
+ .map(|(a, b)| baseline_i8_cos_procedural(a, b))
715
+ .collect();
716
+ let simsimd: Vec<f64> = bench_pairs
717
+ .iter()
718
+ .map(|(a, b)| SimSIMD::cosine(a, b).unwrap())
719
+ .collect();
720
+
721
+ let simsimd_err = calculate_errors(&accurate, &simsimd);
722
+ let procedural_err = calculate_errors(&accurate, &procedural);
723
+
724
+ let mut group = c.benchmark_group(format!("i8/{}d/cosine", dimensions));
725
+ let bytes_per_op = (2 * dimensions * size_of::<i8>() * bench_pairs_count) as u64;
726
+ group.throughput(Throughput::Bytes(bytes_per_op));
727
+ group
728
+ .sample_size(50)
729
+ .measurement_time(Duration::from_secs(2))
730
+ .noise_threshold(0.05);
731
+
732
+ group.bench_function("simsimd", |b| {
733
+ b.iter(|| {
734
+ for (a, b) in &bench_pairs {
735
+ black_box(SimSIMD::cosine(a, b));
736
+ }
737
+ })
738
+ });
739
+ print_error(simsimd_err.0, simsimd_err.1);
740
+
741
+ group.bench_function("procedural", |b| {
742
+ b.iter(|| {
743
+ for (a, b) in &bench_pairs {
744
+ black_box(baseline_i8_cos_procedural(a, b));
745
+ }
746
+ })
747
+ });
748
+ print_error(procedural_err.0, procedural_err.1);
749
+ group.finish();
750
+ }
751
+
752
+ pub fn i8_dot_benchmark(c: &mut Criterion) {
753
+ let dimensions = get_dense_dimensions();
754
+ let bench_pairs_count = (128 * 1024 * 1024) / (dimensions * size_of::<i8>() * 2);
755
+ let bench_pairs: Vec<_> = (0..bench_pairs_count)
756
+ .map(|i| {
757
+ let mut rng = StdRng::seed_from_u64(1000 + i as u64);
758
+ let a: Vec<i8> = (0..dimensions).map(|_| rng.random()).collect();
759
+ let b: Vec<i8> = (0..dimensions).map(|_| rng.random()).collect();
760
+ (a, b)
761
+ })
762
+ .collect();
763
+
764
+ let accurate: Vec<f64> = bench_pairs
765
+ .iter()
766
+ .map(|(a, b)| accurate_i8_dot(a, b))
767
+ .collect();
768
+ let procedural: Vec<f64> = bench_pairs
769
+ .iter()
770
+ .map(|(a, b)| baseline_i8_dot_procedural(a, b))
771
+ .collect();
772
+ let simsimd: Vec<f64> = bench_pairs
773
+ .iter()
774
+ .map(|(a, b)| SimSIMD::dot(a, b).unwrap())
775
+ .collect();
776
+
777
+ let simsimd_err = calculate_errors(&accurate, &simsimd);
778
+ let procedural_err = calculate_errors(&accurate, &procedural);
779
+
780
+ let mut group = c.benchmark_group(format!("i8/{}d/dot", dimensions));
781
+ let bytes_per_op = (2 * dimensions * size_of::<i8>() * bench_pairs_count) as u64;
782
+ group.throughput(Throughput::Bytes(bytes_per_op));
783
+ group
784
+ .sample_size(50)
785
+ .measurement_time(Duration::from_secs(2))
786
+ .noise_threshold(0.05);
787
+
788
+ group.bench_function("simsimd", |b| {
789
+ b.iter(|| {
790
+ for (a, b) in &bench_pairs {
791
+ black_box(SimSIMD::dot(a, b));
792
+ }
793
+ })
794
+ });
795
+ print_error(simsimd_err.0, simsimd_err.1);
796
+
797
+ group.bench_function("procedural", |b| {
798
+ b.iter(|| {
799
+ for (a, b) in &bench_pairs {
800
+ black_box(baseline_i8_dot_procedural(a, b));
801
+ }
802
+ })
803
+ });
804
+ print_error(procedural_err.0, procedural_err.1);
805
+ group.finish();
806
+ }
807
+
808
+ pub fn i8_l2_benchmark(c: &mut Criterion) {
809
+ let dimensions = get_dense_dimensions();
810
+ let bench_pairs_count = (128 * 1024 * 1024) / (dimensions * size_of::<i8>() * 2);
811
+ let bench_pairs: Vec<_> = (0..bench_pairs_count)
812
+ .map(|i| {
813
+ let mut rng = StdRng::seed_from_u64(1000 + i as u64);
814
+ let a: Vec<i8> = (0..dimensions).map(|_| rng.random()).collect();
815
+ let b: Vec<i8> = (0..dimensions).map(|_| rng.random()).collect();
816
+ (a, b)
817
+ })
818
+ .collect();
819
+
820
+ let accurate: Vec<f64> = bench_pairs
821
+ .iter()
822
+ .map(|(a, b)| accurate_i8_l2(a, b))
823
+ .collect();
824
+ let procedural: Vec<f64> = bench_pairs
825
+ .iter()
826
+ .map(|(a, b)| baseline_i8_l2_procedural(a, b))
827
+ .collect();
828
+ let simsimd: Vec<f64> = bench_pairs
829
+ .iter()
830
+ .map(|(a, b)| SimSIMD::euclidean(a, b).unwrap())
831
+ .collect();
832
+
833
+ let simsimd_err = calculate_errors(&accurate, &simsimd);
834
+ let procedural_err = calculate_errors(&accurate, &procedural);
835
+
836
+ let mut group = c.benchmark_group(format!("i8/{}d/l2", dimensions));
837
+ let bytes_per_op = (2 * dimensions * size_of::<i8>() * bench_pairs_count) as u64;
838
+ group.throughput(Throughput::Bytes(bytes_per_op));
839
+ group
840
+ .sample_size(50)
841
+ .measurement_time(Duration::from_secs(2))
842
+ .noise_threshold(0.05);
843
+
844
+ group.bench_function("simsimd", |b| {
845
+ b.iter(|| {
846
+ for (a, b) in &bench_pairs {
847
+ black_box(SimSIMD::euclidean(a, b));
848
+ }
849
+ })
850
+ });
851
+ print_error(simsimd_err.0, simsimd_err.1);
852
+
853
+ group.bench_function("procedural", |b| {
854
+ b.iter(|| {
855
+ for (a, b) in &bench_pairs {
856
+ black_box(baseline_i8_l2_procedural(a, b));
857
+ }
858
+ })
859
+ });
860
+ print_error(procedural_err.0, procedural_err.1);
861
+ group.finish();
862
+ }
863
+
864
+ // endregion: Benchmark Functions
865
+
866
+ fn custom_criterion() -> Criterion {
867
+ Criterion::default().without_plots().noise_threshold(0.05)
868
+ }
869
+
870
+ criterion_group! {
871
+ name = benches;
872
+ config = custom_criterion();
873
+ targets = f32_cosine_benchmark, f32_dot_benchmark, f32_l2_benchmark,
874
+ i8_cosine_benchmark, i8_dot_benchmark, i8_l2_benchmark
875
+ }
876
+ criterion_main!(benches);