Skip to content

Commit 9cd0773

Browse files
feat: add benchmarking for DataFrame operations with Divan
1 parent 8334ca5 commit 9cd0773

3 files changed

Lines changed: 148 additions & 22 deletions

File tree

tests/DataFusionSharp.Benchmark/README.md

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,31 +24,84 @@ Tests DataFrame operations with varying row counts (1, 100, 10,000, 1,000,000 ro
2424
## Results
2525

2626
```
27-
BenchmarkDotNet v0.15.8, Linux Debian GNU/Linux 12 (bookworm)
28-
Intel Core i7-10510U CPU 1.80GHz, 1 CPU, 8 logical and 4 physical cores
29-
.NET SDK 10.0.100
30-
[Host] : .NET 8.0.20 (8.0.20, 8.0.2025.41914), X64 RyuJIT x86-64-v3
31-
DefaultJob : .NET 8.0.20 (8.0.20, 8.0.2025.41914), X64 RyuJIT x86-64-v3
32-
33-
34-
| Method | RowCount | Mean | Error | StdDev | Gen0 | Allocated |
35-
|--------------- |--------- |-------------:|-----------:|-----------:|-------:|----------:|
36-
| CountAsync | 1 | 1,024.932 us | 20.3613 us | 40.6638 us | - | 208 B |
37-
| GetSchemaAsync | 1 | 4.764 us | 0.0624 us | 0.0554 us | 0.2594 | 1112 B |
38-
| CollectAsync | 1 | 568.739 us | 11.0437 us | 14.3599 us | - | 1712 B |
39-
| CountAsync | 100 | 1,022.946 us | 20.1709 us | 47.1487 us | - | 208 B |
40-
| GetSchemaAsync | 100 | 4.789 us | 0.0741 us | 0.0693 us | 0.2594 | 1112 B |
41-
| CollectAsync | 100 | 568.024 us | 11.3468 us | 12.6120 us | - | 1712 B |
42-
| CountAsync | 10000 | 955.131 us | 18.9728 us | 21.8491 us | - | 208 B |
43-
| GetSchemaAsync | 10000 | 4.916 us | 0.0738 us | 0.0616 us | 0.2594 | 1112 B |
44-
| CollectAsync | 10000 | 620.891 us | 12.0163 us | 12.3399 us | - | 2128 B |
45-
| CountAsync | 1000000 | 4,192.886 us | 60.6931 us | 56.7723 us | - | 208 B |
46-
| GetSchemaAsync | 1000000 | 4.697 us | 0.0322 us | 0.0269 us | 0.2594 | 1112 B |
47-
| CollectAsync | 1000000 | 5,780.556 us | 90.7016 us | 84.8423 us | 7.8125 | 54571 B |
27+
BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
28+
Intel Xeon Platinum 8275CL CPU 3.00GHz, 1 CPU, 2 logical cores and 1 physical core
29+
.NET SDK 10.0.102
30+
[Host] : .NET 8.0.23 (8.0.23, 8.0.2325.60607), X64 RyuJIT x86-64-v4
31+
DefaultJob : .NET 8.0.23 (8.0.23, 8.0.2325.60607), X64 RyuJIT x86-64-v4
4832
```
4933

34+
| Method | RowCount | Mean | Error | StdDev | Gen0 | Allocated |
35+
|----------------|----------|-------------:|-----------:|-----------:|-------:|----------:|
36+
| CountAsync | 1 | 500.201 us | 0.3151 us | 0.2631 us | - | 208 B |
37+
| GetSchemaAsync | 1 | 4.112 us | 0.0136 us | 0.0120 us | 0.0534 | 1112 B |
38+
| CollectAsync | 1 | 324.755 us | 0.7113 us | 0.6306 us | - | 1712 B |
39+
| CountAsync | 100 | 500.173 us | 0.2189 us | 0.1828 us | - | 208 B |
40+
| GetSchemaAsync | 100 | 4.059 us | 0.0168 us | 0.0157 us | 0.0534 | 1112 B |
41+
| CollectAsync | 100 | 323.459 us | 0.7462 us | 0.6231 us | - | 1712 B |
42+
| CountAsync | 10000 | 537.283 us | 0.9703 us | 0.8601 us | - | 208 B |
43+
| GetSchemaAsync | 10000 | 3.914 us | 0.0087 us | 0.0072 us | 0.0534 | 1112 B |
44+
| CollectAsync | 10000 | 357.141 us | 1.4852 us | 1.3893 us | - | 2128 B |
45+
| CountAsync | 1000000 | 3,358.221 us | 25.1665 us | 23.5408 us | - | 208 B |
46+
| GetSchemaAsync | 1000000 | 3.919 us | 0.0103 us | 0.0091 us | 0.0534 | 1112 B |
47+
| CollectAsync | 1000000 | 3,318.863 us | 65.2399 us | 84.8303 us | - | 54569 B |
48+
49+
## Reference Results From Native DataFusion
50+
51+
```
52+
dataframe fastest │ slowest │ median │ mean │ samples │ iters
53+
├─ collect │ │ │ │ │
54+
│ ├─ 1 192.8 µs │ 742.9 µs │ 203.6 µs │ 213.8 µs │ 100 │ 100
55+
│ ├─ 100 196 µs │ 251.6 µs │ 201.8 µs │ 209.8 µs │ 100 │ 100
56+
│ ├─ 10000 239.7 µs │ 293.9 µs │ 246.3 µs │ 253.3 µs │ 100 │ 100
57+
│ ╰─ 1000000 1.878 ms │ 5.764 ms │ 5.038 ms │ 4.523 ms │ 100 │ 100
58+
├─ count │ │ │ │ │
59+
│ ├─ 1 425.4 µs │ 753.7 µs │ 451.4 µs │ 454.3 µs │ 100 │ 100
60+
│ ├─ 100 430.1 µs │ 489.4 µs │ 449 µs │ 451.5 µs │ 100 │ 100
61+
│ ├─ 10000 464.7 µs │ 531.9 µs │ 489 µs │ 491.4 µs │ 100 │ 100
62+
│ ╰─ 1000000 2.852 ms │ 3.435 ms │ 3.19 ms │ 3.231 ms │ 100 │ 100
63+
╰─ get_schema │ │ │ │ │
64+
├─ 1 3.081 ns │ 3.389 ns │ 3.356 ns │ 3.354 ns │ 100 │ 102400
65+
├─ 100 3.08 ns │ 3.368 ns │ 3.355 ns │ 3.354 ns │ 100 │ 102400
66+
├─ 10000 3.347 ns │ 3.367 ns │ 3.349 ns │ 3.351 ns │ 100 │ 102400
67+
╰─ 1000000 3.08 ns │ 26.05 ns │ 3.355 ns │ 3.621 ns │ 100 │ 102400
68+
```
69+
70+
## Comparison
71+
72+
| Method | RowCount | .NET (µs) | Rust (µs) | Δ Time (µs) |
73+
|--------|----------:|----------:|----------:|-------------:|
74+
| Count | 1 | 500.201 | 454.300 | **+45.901** |
75+
| Count | 100 | 500.173 | 451.500 | **+48.673** |
76+
| Count | 10,000 | 537.283 | 491.400 | **+45.883** |
77+
| Count | 1,000,000 | 3,358.221 | 3,231.000 | **+127.221** |
78+
79+
| Method | RowCount | .NET (µs) | Rust (µs) | Δ Time (µs) |
80+
|---------|----------:|----------:|----------:|---------------:|
81+
| Collect | 1 | 324.755 | 213.800 | **+110.955** |
82+
| Collect | 100 | 323.459 | 209.800 | **+113.659** |
83+
| Collect | 10,000 | 357.141 | 253.300 | **+103.841** |
84+
| Collect | 1,000,000 | 3,318.863 | 4,523.000 | **−1,204.137** |
85+
86+
| Method | RowCount | .NET (µs) | Rust (µs) | Δ Time (µs) |
87+
|-----------|----------:|----------:|----------:|------------:|
88+
| GetSchema | 1 | 4.112 | 0.003354 | **+4.109** |
89+
| GetSchema | 100 | 4.059 | 0.003354 | **+4.056** |
90+
| GetSchema | 10,000 | 3.914 | 0.003351 | **+3.911** |
91+
| GetSchema | 1,000,000 | 3.919 | 0.003621 | **+3.915** |
92+
93+
## Conclusion
94+
95+
Overall, the benchmarks show that the .NET wrapper adds a **mostly constant FFI overhead** on top of the native Rust execution:
96+
97+
* **~4 µs per call** for trivial operations (`GetSchema`).
98+
* **~45–50 µs** for `Count`.
99+
* **~100–115 µs** for `Collect` largely independent of row count.
100+
101+
For small and medium workloads this overhead dominates, clearly visible when compared to nanosecond-level native Rust timings, while for large workloads (1M rows) the execution cost begins to outweigh the boundary cost and results converge, with some variance even allowing .NET to match or exceed Rust in isolated cases.
102+
50103
## Notes
51104

52105
- Benchmarks measure end-to-end time including native interop overhead
53-
- Memory diagnostics enabled via `[MemoryDiagnoser]`
106+
- Native Rust benchmarks source available in [`./native-reference-bench`](./native-reference-bench) directory
54107
- Results may vary based on hardware and system load
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "native-reference-bench"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
datafusion = "52.1.0"
8+
datafusion-sharp-native = { path = "../../../native" }
9+
tokio = { version = "1.49.0", features = ["rt-multi-thread"] }
10+
divan = "0.1.21"
11+
12+
[[bench]]
13+
name = "dataframe"
14+
harness = false
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use datafusion::prelude::SessionContext;
2+
use divan::{Bencher, black_box};
3+
4+
fn main() {
5+
divan::main();
6+
}
7+
8+
const ROWS_COUNTS: &[u32] = &[1, 100, 10000, 1000000];
9+
10+
fn create_runtime() -> tokio::runtime::Runtime {
11+
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()
12+
}
13+
14+
fn generate_dataframe(runtime: &tokio::runtime::Runtime, row_count: u32) -> datafusion::prelude::DataFrame {
15+
runtime.block_on(async move {
16+
let session_context = SessionContext::new();
17+
let sql = format!("SELECT series.value AS id FROM generate_series(1, {row_count}) AS series");
18+
session_context.sql(&sql).await.unwrap()
19+
})
20+
}
21+
22+
#[divan::bench(args = ROWS_COUNTS)]
23+
fn count(bencher: Bencher, row_count: u32) {
24+
let runtime = create_runtime();
25+
let df = generate_dataframe(&runtime, row_count);
26+
27+
bencher.bench_local(move || {
28+
let df = df.clone();
29+
runtime.block_on(async move {
30+
let result = df.count().await.unwrap();
31+
black_box(result);
32+
});
33+
});
34+
}
35+
36+
#[divan::bench(args = ROWS_COUNTS)]
37+
fn get_schema(bencher: Bencher, row_count: u32) {
38+
let runtime = create_runtime();
39+
let df = generate_dataframe(&runtime, row_count);
40+
41+
bencher.bench_local(move || {
42+
let result = df.schema();
43+
black_box(result);
44+
});
45+
}
46+
47+
#[divan::bench(args = ROWS_COUNTS)]
48+
fn collect(bencher: Bencher, row_count: u32) {
49+
let runtime = create_runtime();
50+
let df = generate_dataframe(&runtime, row_count);
51+
52+
bencher.bench_local(move || {
53+
let df = df.clone();
54+
runtime.block_on(async move {
55+
let result = df.collect().await.unwrap();
56+
black_box(result);
57+
});
58+
});
59+
}

0 commit comments

Comments
 (0)