Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Serial pipeline measurements

Historical measurements; see Profiling the compiler for the profiling guide.

Serial pipeline callbacks

scripts/bench_pipelines.py compares an inline range/filter/map/fold loop with fused pipelines using named callbacks, closure literals, and immutable scalar captures. All variants do the same checked arithmetic: filter even values, square each accepted value modulo 1,000, and add the results. A second pair compares a loop with a closure-literal map whose body is a conditional expression, against its own inline loop. Run:

python3 -B scripts/bench_pipelines.py target/debug/plenty --items 100000000 --runs 7

The compiler executable can be a debug build: the harness requests optimized native output with --release, disables caches, checks every result, interleaves executables and reverses their order on alternate rounds. Compilation and cold checking of 100 separate functions are measured separately. This is a repeatable measurement tool, not a timing-sensitive unit test.

Issue #399 recorded the original named-callback pipeline at 1.906 times its inline-loop baseline. A fresh pre-change run on this host measured 0.160906 s for the inline loop and 0.283777 s for the pipeline (1.764 times, medians of seven executions over 100 million items). The direct callback calls survived fusion. Bounded scalar leaf substitution removes those calls, while SSA mirrors of immutable scalar captures expose constant divisors to native optimization.

Only straight-line scalar leaf bodies with at most 24 meaningful operations qualify. Capture mirrors follow local assignments and joins; exclusive borrowing disables them for the affected local. Checked arithmetic keeps the original callee source location. Debug code and larger or effectful callbacks retain their existing behavior. Broader callback substitution needs separate evidence.

On the same host, a quiet post-change run over 100 million items (seven interleaved executions per variant) measured:

VariantNative medianRelative to inline loopCold check, 100 functionsSingle-program compile
Inline loop0.156573 s1.0000.011199 s0.046002 s
Named callbacks0.155743 s0.9950.011768 s0.043120 s
Closure literals0.169209 s1.0810.023402 s0.042156 s
Scalar captures0.154756 s0.9880.023753 s0.044352 s

All executions returned 24500000000; all three pipeline forms meet the proposal’s within-10% runtime target for this workload. Closure literals still cost about 8% here, and checking 100 closure-heavy functions costs roughly twice the named-function variant. Single-program compilation is one observation per variant, not evidence of a compilation speedup. The machine was not frequency- locked or CPU-pinned; small runtime differences around parity are noise. These results do not promise the same ratios for larger, branching or mutable callbacks.

Closure-literal splicing

Closure literals written in a chain are now lowered in the consumer’s loop rather than as hidden functions (B75 stage 2). Before timing, --dump ops showed no ClosureNew or ClosureCall in the fused functions, and the optimized asm of the filter/map/fold closure variant has no call inside the loop. The branching variant’s asm has 107 instructions against 108 for its inline loop.

Both compilers were measured on this host back to back, pinned with taskset -c 0,2,4,6,8,10,12,14, over 100 million items with seven interleaved executions per variant:

VariantBefore: medianBefore: ratioAfter: medianAfter: ratio
Inline loop0.155813 s1.0000.157660 s1.000
Named callbacks0.157610 s1.0120.156564 s0.993
Closure literals0.159477 s1.0240.156964 s0.996
Scalar captures0.159048 s1.0210.156939 s0.995
Branching loop0.223247 s1.0000.222619 s1.000
Branching closure0.234639 s1.0510.225732 s1.014

Cold checks of 100 functions, medians of seven runs:

VariantBeforeAfter
Inline loop0.003607 s0.004034 s
Named callbacks0.003654 s0.003728 s
Closure literals0.005867 s0.005057 s
Scalar captures0.007005 s0.004893 s
Branching loop0.004116 s0.003629 s
Branching closure0.005514 s0.004041 s

All runs returned 24500000000 and 16850000993. Closure literals now run within 1.5% of their inline loops. Checking closure-heavy functions fell from about 1.6 to 1.4 times the named variant; the remainder is per-literal capture and splice lowering. Cold-check numbers are one series per compiler and vary by roughly 10% between runs.

Fluent generators and explicit conversion

scripts/bench_pipeline_generators.py measures the same filter/map/fold workload with range and generator sources, local fused consumption, explicit .generator() conversion. The compiler executable requests optimized native output, disables caches, checks equal results, and alternates execution order as in the callback benchmark:

python3 -B scripts/bench_pipeline_generators.py target/debug/plenty --items 5000000 --runs 7

A quiet run on this host measured the following medians. Every execution returned 1225000000.

VariantNative median
Inline range loop0.010011 s
Local fused range pipeline0.009781 s
Inline generator loop0.042060 s
Generator source through iter(...)0.044602 s
Direct fluent generator source0.044454 s
Range pipeline converted with .generator()0.027202 s
Generator-source pipeline converted with .generator()0.065896 s
std.iter chain with the same generator source (removed module; see the discussion after the table)0.105389 s

Direct fluent syntax has the same cost as wrapping the source in iter(...) (0.997 times here), and costs 1.057 times the inline generator loop. Explicit conversion adds a resumable frame: the converted range pipeline costs 2.781 times local consumption. With the same generator source, the converted pipeline took 0.625 times the runtime of a nested std.iter adapter chain. That row was measured at commit 57982e0f; std.iter has since been removed, and the script no longer runs it. Internal filter/map adapters are fused into one frame; resuming the source generator and the converted frame still has a cost. Keep local consumers fused when the stream does not need to escape.

The existing 100-million-item callback benchmark was also rerun with seven interleaved executions. Named callbacks, closure literals and scalar captures measured 0.999, 1.066 and 1.000 times the inline loop respectively, retaining the within-10% target. Compiler tests separately check a single converted frame with inlined scalar callbacks, and zero tracked allocations while constructing, moving, resuming and dropping an owned scalar pipeline. These timings are observations on a machine without frequency locking or CPU pinning; they do not establish universal ratios or allocation behavior for heap-owning source items.