Skip to content

Commit 70a4ec3

Browse files
committed
Add DuckDB conversion benchmark and performance conclusions
1 parent 440a546 commit 70a4ec3

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

‎bench/ctable/from_to_duckdb.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Benchmark DuckDB <-> CTable conversions with bool/int64/UTF-8 columns.
2+
3+
Run in the blosc2 environment::
4+
5+
conda run -n blosc2 python bench/ctable/from_to_duckdb.py
6+
7+
DuckDB input is an already materialized in-memory table. Imports include query
8+
execution and CTable compression. Exports create a materialized DuckDB table,
9+
not merely a lazy registration; the Arrow-table route includes to_arrow().
10+
Generation, validation, GC and cleanup are outside timings. Every result is
11+
checked against the original Arrow input. Report medians after one warmup.
12+
"""
13+
14+
import argparse
15+
import gc
16+
import platform
17+
import statistics
18+
from time import perf_counter
19+
20+
import duckdb
21+
import numpy as np
22+
import pyarrow as pa
23+
from from_to_arrow import check_result, make_arrow
24+
25+
import blosc2
26+
27+
28+
# Observations from a Darwin arm64 run (Python 3.14.4, DuckDB 1.5.2,
29+
# PyArrow 24.0.0, 8 threads, median of 5 runs after warmup):
30+
# At 100,000 rows, DuckDB -> CTable took 11.27 ms via direct PyCapsule,
31+
# 10.89 ms via an Arrow table, and 16.04 ms via a 65,536-row Arrow reader.
32+
# CTable -> materialized DuckDB took 7.88 ms directly versus 7.82 ms via
33+
# to_arrow(). Direct streams use provider defaults, so the reader comparison
34+
# also reflects different batch boundaries, not just interface overhead.
35+
# DuckDB already uses Arrow interchange and our direct UTF-8 buffer writer.
36+
# These results reveal no obvious additional DuckDB-specific fast path.
37+
# Batch sizing and compressed-storage writes are candidates for further work,
38+
# but profiling would be needed to establish the remaining bottlenecks.
39+
40+
41+
def measure(operation, source, connection, repeats):
42+
samples = []
43+
for repetition in range(repeats + 1):
44+
gc.collect()
45+
start = perf_counter()
46+
result = operation()
47+
elapsed = perf_counter() - start
48+
try:
49+
actual = result.to_arrow_table() if isinstance(result, duckdb.DuckDBPyRelation) else result
50+
check_result(actual, source)
51+
finally:
52+
if isinstance(result, blosc2.CTable):
53+
result.close()
54+
else:
55+
connection.execute("DROP TABLE IF EXISTS output_table")
56+
connection.unregister("input_data")
57+
del result
58+
if repetition:
59+
samples.append(elapsed)
60+
return statistics.median(samples)
61+
62+
63+
def materialize(connection, table, via_arrow):
64+
source = table.to_arrow() if via_arrow else table
65+
connection.register("input_data", source)
66+
connection.execute("CREATE TEMP TABLE output_table AS SELECT * FROM input_data")
67+
return connection.table("output_table")
68+
69+
70+
def main():
71+
parser = argparse.ArgumentParser(description=__doc__)
72+
parser.add_argument("--sizes", type=int, nargs="+", default=[1_000, 10_000, 100_000])
73+
parser.add_argument("--repeats", type=int, default=5)
74+
parser.add_argument("--batch-size", type=int, default=65_536)
75+
parser.add_argument("--nthreads", type=int, default=blosc2.nthreads)
76+
args = parser.parse_args()
77+
if min(*args.sizes, args.repeats, args.batch_size, args.nthreads) < 1:
78+
parser.error("sizes, repeats, batch-size and nthreads must be positive")
79+
blosc2.set_nthreads(args.nthreads)
80+
print(f"Python {platform.python_version()} | {platform.system()} {platform.machine()}")
81+
print(f"Blosc2 {blosc2.__version__} | DuckDB {duckdb.__version__} | PyArrow {pa.__version__}")
82+
print(f"NumPy {np.__version__} | threads: {blosc2.nthreads} | repeats: {args.repeats} + warmup")
83+
print(f"Explicit reader batch size: {args.batch_size:,}; direct streams use provider defaults")
84+
print("Schema: flag: bool, count: int64, name: utf8 (10,000 labels; no nulls)")
85+
print("CTable -> DuckDB timings include CREATE TABLE AS SELECT (full materialization).")
86+
print(f"\n{'Rows':>10} {'Operation':<44} {'Median ms':>10} {'Mrows/s':>9}")
87+
with duckdb.connect(config={"threads": args.nthreads}) as connection:
88+
for rows in args.sizes:
89+
source = make_arrow(rows, args.batch_size)
90+
connection.register("arrow_source", source)
91+
connection.execute("CREATE OR REPLACE TEMP TABLE source_table AS SELECT * FROM arrow_source")
92+
connection.unregister("arrow_source")
93+
with blosc2.CTable.from_arrow(source) as table:
94+
jobs = [
95+
(
96+
"DuckDB -> CTable (direct PyCapsule)",
97+
lambda: blosc2.CTable.from_arrow(connection.table("source_table")),
98+
),
99+
(
100+
"DuckDB -> CTable (Arrow reader)",
101+
lambda: blosc2.CTable.from_arrow(
102+
connection.table("source_table").to_arrow_reader(args.batch_size)
103+
),
104+
),
105+
(
106+
"DuckDB -> CTable (Arrow table)",
107+
lambda: blosc2.CTable.from_arrow(connection.table("source_table").to_arrow_table()),
108+
),
109+
("CTable -> DuckDB (direct PyCapsule)", lambda: materialize(connection, table, False)),
110+
("CTable -> DuckDB (via to_arrow)", lambda: materialize(connection, table, True)),
111+
]
112+
for name, operation in jobs:
113+
elapsed = measure(operation, source, connection, args.repeats)
114+
print(f"{rows:>10,} {name:<44} {elapsed * 1000:>10.2f} {rows / elapsed / 1e6:>9.2f}")
115+
connection.execute("DROP TABLE source_table")
116+
117+
118+
if __name__ == "__main__":
119+
main()

0 commit comments

Comments
 (0)