phibble

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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.

1,000,000 rows: elapsed time and memory by implementation
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

From the repository root
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.