Benchmarks
Workloads
Tables
Table load
Results
Load events.csv and dims.csv into each backend. DataFrame: CSV read with the phit reader. SqlTable: stored SQLite tables opened with a LIMIT 0 query and a 1,000-row type sample. PolarsTable: CSV read into a DataFrame and copied into Polars. Memory is the baseline included in every other table record.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv');
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims');
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv'));
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 341 ms | 337 ms – 342 ms | 118 MiB | 149 MiB | 88.0 MiB |
| SqlTable | 4.77 ms | 4.61 ms – 4.88 ms | 15.6 MiB | 47.6 MiB | 4.00 MiB |
| PolarsTable | 445 ms | 441 ms – 449 ms | 167 MiB | 210 MiB | 88.0 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from the first load call to both tables held. Process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' tables-load
- Raw record
- site/benchmarks/tables-load.json
- Source
- bench/src/Workloads.php
Grouped summary
Results
Count, mean, and sum of x with NA removed, by key: 10,000 groups at 1,000,000 rows.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 10,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events ->groupBy(['key']) ->summarise(['n' => n(), 'mean_x' => mean(col('x'), na_rm: true), 'sum_x' => sum(col('x'), na_rm: true)]) ->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events ->groupBy(['key']) ->summarise(['n' => n(), 'mean_x' => mean(col('x'), na_rm: true), 'sum_x' => sum(col('x'), na_rm: true)]) ->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events ->groupBy(['key']) ->summarise(['n' => n(), 'mean_x' => mean(col('x'), na_rm: true), 'sum_x' => sum(col('x'), na_rm: true)]) ->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 204 ms | 194 ms – 208 ms | 178 MiB | 209 MiB | 156 MiB |
| SqlTable | 751 ms | 741 ms – 760 ms | 18.1 MiB | 50.1 MiB | 6.00 MiB |
| PolarsTable | 19.6 ms | 19.2 ms – 19.7 ms | 163 MiB | 206 MiB | 84.0 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' group-summarise
- Raw record
- site/benchmarks/group-summarise.json
- Source
- bench/src/Workloads.php
Grouped summary, closures
Results
The grouped summary as PHP closures over each group. SqlTable and PolarsTable collect the rows and run the closures in a DataFrame.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 10,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events ->groupBy(['key']) ->summarise([ 'n' => fn (Table $g) => $g->nrow(), 'sum_x' => fn (Table $g) => array_sum(array_filter($g->column('x'), fn ($v) => $v !== null)), ]) ->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events ->groupBy(['key']) ->summarise([ 'n' => fn (Table $g) => $g->nrow(), 'sum_x' => fn (Table $g) => array_sum(array_filter($g->column('x'), fn ($v) => $v !== null)), ]) ->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events ->groupBy(['key']) ->summarise([ 'n' => fn (Table $g) => $g->nrow(), 'sum_x' => fn (Table $g) => array_sum(array_filter($g->column('x'), fn ($v) => $v !== null)), ]) ->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 510 ms | 508 ms – 514 ms | 178 MiB | 209 MiB | 156 MiB |
| SqlTable | 2.55 s | 2.54 s – 2.58 s | 577 MiB | 609 MiB | 568 MiB |
| PolarsTable | 836 ms | 830 ms – 849 ms | 274 MiB | 319 MiB | 182 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' group-summarise-closure
- Raw record
- site/benchmarks/group-summarise-closure.json
- Source
- bench/src/Workloads.php
Window functions
Results
lag(x), a running sum of id, and the row number, by key, ordered by t.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events ->groupBy(['key']) ->arrange(['t']) ->mutate(['prev' => lag(col('x')), 'run' => cumsum(col('id')), 'rn' => row_number()]) ->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events ->groupBy(['key']) ->arrange(['t']) ->mutate(['prev' => lag(col('x')), 'run' => cumsum(col('id')), 'rn' => row_number()]) ->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events ->groupBy(['key']) ->arrange(['t']) ->mutate(['prev' => lag(col('x')), 'run' => cumsum(col('id')), 'rn' => row_number()]) ->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 1.08 s | 1.06 s – 1.09 s | 338 MiB | 370 MiB | 320 MiB |
| SqlTable | 5.84 s | 5.83 s – 5.94 s | 593 MiB | 626 MiB | 594 MiB |
| PolarsTable | 525 ms | 514 ms – 527 ms | 388 MiB | 444 MiB | 168 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' grouped-window
- Raw record
- site/benchmarks/grouped-window.json
- Source
- bench/src/Workloads.php
Filter
Results
Rows with positive x and g in three of eight labels.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 184,129
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events ->filter(col('x')->gt(0)->and(col('g')->isIn(['alpha', 'beta', 'gamma']))) ->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events ->filter(col('x')->gt(0)->and(col('g')->isIn(['alpha', 'beta', 'gamma']))) ->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events ->filter(col('x')->gt(0)->and(col('g')->isIn(['alpha', 'beta', 'gamma']))) ->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 168 ms | 167 ms – 170 ms | 163 MiB | 195 MiB | 150 MiB |
| SqlTable | 441 ms | 440 ms – 445 ms | 114 MiB | 146 MiB | 110 MiB |
| PolarsTable | 58.3 ms | 57.8 ms – 59.1 ms | 163 MiB | 206 MiB | 84.0 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' filter
- Raw record
- site/benchmarks/filter.json
- Source
- bench/src/Workloads.php
Mutate
Results
One computed double column and one logical column on every row.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events ->mutate(['y' => col('x')->mul(2)->add(col('id')), 'big' => col('x')->gt(50)]) ->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events ->mutate(['y' => col('x')->mul(2)->add(col('id')), 'big' => col('x')->gt(50)]) ->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events ->mutate(['y' => col('x')->mul(2)->add(col('id')), 'big' => col('x')->gt(50)]) ->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 275 ms | 274 ms – 276 ms | 148 MiB | 180 MiB | 134 MiB |
| SqlTable | 2.38 s | 2.37 s – 2.38 s | 578 MiB | 610 MiB | 578 MiB |
| PolarsTable | 371 ms | 370 ms – 372 ms | 259 MiB | 307 MiB | 152 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' mutate
- Raw record
- site/benchmarks/mutate.json
- Source
- bench/src/Workloads.php
Left join
Results
Events left-joined to dims on key.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events->join($dims, ['key'], 'left')->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events->join($dims, ['key'], 'left')->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events->join($dims, ['key'], 'left')->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 327 ms | 324 ms – 342 ms | 279 MiB | 311 MiB | 272 MiB |
| SqlTable | 3.45 s | 3.44 s – 3.62 s | 624 MiB | 657 MiB | 624 MiB |
| PolarsTable | 481 ms | 478 ms – 486 ms | 386 MiB | 446 MiB | 186 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' join
- Raw record
- site/benchmarks/join.json
- Source
- bench/src/Workloads.php
Sort
Results
Events sorted by x descending, then id.
- Workload
- 1,000,000 rows of events and 100,000 rows of dims
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv');$dims = DataFrame::fromCsv('dims.csv'); $rows = $events->arrange([['x', 'desc'], 'id'])->collect()->nrow();
$pdo = new PDO('sqlite:bench.sqlite');$events = SqlTable::table($pdo, 'events');$dims = SqlTable::table($pdo, 'dims'); $rows = $events->arrange([['x', 'desc'], 'id'])->collect()->nrow();
$events = PolarsTable::fromTable(DataFrame::fromCsv('events.csv'));$dims = PolarsTable::fromTable(DataFrame::fromCsv('dims.csv')); $rows = $events->arrange([['x', 'desc'], 'id'])->collect()->nrow();
1,000,000 rows of events and 100,000 rows of dims: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 877 ms | 860 ms – 897 ms | 430 MiB | 461 MiB | 413 MiB |
| SqlTable | 2.49 s | 2.48 s – 2.51 s | 547 MiB | 580 MiB | 546 MiB |
| PolarsTable | 421 ms | 416 ms – 429 ms | 259 MiB | 307 MiB | 120 MiB |
Methods
- Implementations
- DataFrame: Phibble\DataFrame: PHP arrays; SqlTable: Phibble\Sql\SqlTable on a SQLite 3.45.1 file, through PDO; PolarsTable: Phibble\Polars\PolarsTable, php_polars 0.2.1
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1, SqlTable 1, PolarsTable Polars' default thread pool, one thread per core (2 here)
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from loaded input tables to the result collected into PHP and its rows counted; lazy backends run their query within the timing. Loading (measured in Table load) and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded. SqlTable tables remain in the SQLite file, read through a page cache of about 2 MiB (SQLite default cache_size, memory mapping off). The kernel file cache counts in neither memory measure; SqlTable memory is mostly the rows collected into PHP.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' arrange
- Raw record
- site/benchmarks/arrange.json
- Source
- bench/src/Workloads.php
CSV
CSV read, declared types
Results
events.csv read into a DataFrame with column types declared as 'iicdT'. Reading with guessed types: phit CSV benchmark (https://github.com/phpstats/phit/blob/main/docs/benchmarks/csv-results.json).
- Workload
- 1,000,000 rows, 5 columns: id int, key int, g string, x double with about 2% NA, t datetime
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
Phibble\Io\NativeCsv::enable(false); $rows = DataFrame::fromCsv('events.csv', types: 'iicdT')->nrow();
$rows = DataFrame::fromCsv('events.csv', types: 'iicdT')->nrow();
1,000,000 rows, 5 columns: id int, key int, g string, x double with about 2% NA, t datetime: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| PHP reader | 1.38 s | 1.37 s – 1.38 s | 143 MiB | 174 MiB | 144 MiB |
| phit reader | 219 ms | 217 ms – 221 ms | 111 MiB | 143 MiB | 92.0 MiB |
Methods
- Implementations
- PHP reader: phibble's CSV reader in PHP; phit reader: phibble with the phit 0.4.3 extension's native reader
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- PHP reader 1, phit reader 1
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from the call to the whole table held in PHP values. Process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' csv-typed
- Raw record
- site/benchmarks/csv-typed.json
- Source
- bench/src/Workloads.php
Chunked CSV read
Results
events.csv read in chunks of 100,000 rows with declared types, one chunk held at a time.
- Workload
- 1,000,000 rows, 5 columns: id int, key int, g string, x double with about 2% NA, t datetime
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
Phibble\Io\NativeCsv::enable(false); $rows = 0;foreach (DataFrame::csvChunks('events.csv', chunkSize: 100_000, types: 'iicdT') as $chunk) { $rows += $chunk->nrow();}
$rows = 0;foreach (DataFrame::csvChunks('events.csv', chunkSize: 100_000, types: 'iicdT') as $chunk) { $rows += $chunk->nrow();}
1,000,000 rows, 5 columns: id int, key int, g string, x double with about 2% NA, t datetime: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| PHP reader | 1.38 s | 1.37 s – 1.38 s | 50.0 MiB | 80.8 MiB | 46.1 MiB |
| phit reader | 221 ms | 220 ms – 221 ms | 56.4 MiB | 88.4 MiB | 24.0 MiB |
Methods
- Implementations
- PHP reader: phibble's CSV reader in PHP; phit reader: phibble with the phit 0.4.3 extension's native reader
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- PHP reader 1, phit reader 1
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing from the call to the whole table held in PHP values. Process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' csv-chunked
- Raw record
- site/benchmarks/csv-chunked.json
- Source
- bench/src/Workloads.php
CSV write
Results
The events table written to a CSV file with writeCsv(), in PHP.
- Workload
- 1,000,000 rows, 5 columns: id int, key int, g string, x double with about 2% NA, t datetime
- Dataset
- 1,000,000 rows, generated by php bench/generate.php 1000000, 45.8 MB
- Output check
-
- Result rows: 1,000,000
- Note: Identical result row counts across implementations and runs.
$events = DataFrame::fromCsv('events.csv', types: 'iicdT'); $events->writeCsv($out);
1,000,000 rows, 5 columns: id int, key int, g string, x double with about 2% NA, t datetime: elapsed time, peak anonymous memory, peak resident set size, and PHP allocator peak per implementation, median of 7 runs.
| Implementation | Elapsed time | Fastest – slowest run | Peak anonymous memory | Peak resident set size | PHP allocator peak |
|---|---|---|---|---|---|
| DataFrame | 2.81 s | 2.80 s – 2.82 s | 111 MiB | 143 MiB | 92.0 MiB |
Methods
- Implementations
- DataFrame: DataFrame::writeCsv()
- Machine
- 2 AMD Zen 5 cores, 7.8 GB RAM, Linux
- Software
- PHP 8.5.12, phibble v0.6.1-12-g7d3b3c3, phit 0.4.3, php_polars 0.2.1, SQLite 3.45.1
- Threads
- DataFrame 1
- Repetitions
- 7 timed runs, 1 warm-up run
- Statistic
- Median over the timed runs
- Time measure
- Elapsed time, in ms
- Timing boundaries
- Fresh PHP process per run. Timing of the writeCsv() call. Reading the input and process startup excluded. One warm-up per implementation, then 7 rounds with implementations alternating in rotated order.
- Time spent in
- The PHP process; SQLite and Polars run in it as linked libraries.
- Cache
- Warm
- Memory metric
- Peak anonymous memory; Peak resident set size; PHP allocator peak. Peak anonymous memory: RssAnon from /proc/PID/status, sampled every millisecond by peakmem (phit bench/peakmem.c), whole process, inputs included. Peak resident set size: getrusage() maximum, mapped file pages included. PHP allocator peak: memory_get_peak_usage(true) at the end of the run; SQLite and Polars allocations excluded.
- Recorded
- 2026-11-01, commit 7d3b3c3
Reproduction
cc -O2 -o ~/.local/bin/peakmem bench/peakmem.c # in a phit checkout
bin/bench-record --php='php -d extension=phit.so -d extension=php_polars.so' csv-write
- Raw record
- site/benchmarks/csv-write.json
- Source
- bench/src/Workloads.php
Not measured
No benchmark covers SqlTable on PostgreSQL or MySQL, or any run on macOS or Windows.