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

Function-alignment follow-up

Issue #437’s placement probe found a repeatable scan-throughput difference with identical function bytes. The next experiment tested a small potential remedy: raise the alignment of every generated function to 16, 32 or 64 bytes.

Do not adopt a blanket function-alignment change from these results. It improves some placements and worsens others. In particular, 64-byte alignment makes the one-column scan consistently slower than its best unaligned placement. This follow-up changes the experiment and documentation, not compiler behavior. Issue #437 remains open at low priority.

The subsequent online research connects the observed cycle groups to documented fetch-block constraints and a closely matching upstream incident. It finds no verified small remedy and recommends deferring further optimization rather than applying blanket alignment.

Available backend control

The pinned Cranelift 0.131.1 exposes log2_min_function_alignment. Its x86-64 backend specifies a minimum alignment of one byte and a preferred alignment of 32 bytes, but ObjectModule uses the compiled buffer’s required alignment. Our baseline object therefore has byte-aligned functions (some sections need eight-byte alignment for constants). Preferred alignment is not a guarantee that the object linker places every function on a 32-byte boundary.

This version has no public loop-header alignment control. Its bb_padding_log2_minus_one option inserts dummy zero bytes for code-generator stress testing; it is not a production loop-alignment setting. These observations come from the pinned crate’s shared/settings.rs, isa/x64/inst/mod.rs, machinst/buffer.rs, and the object backend, rather than an assumption about what another version or Wasmtime does.

Controlled comparison

The compiler was freshly built from 68108734 without allocation instrumentation. Each workload was compiled once. GNU objcopy then raised all function-section alignments, and GNU ld linked each variant at eight requested text addresses. The baseline retains the original object, including constant-pool alignment. The probe verifies unchanged function-section contents and actual function addresses. It never inserts instructions or changes arithmetic semantics.

Measurements use the AMD Ryzen 7 7840HS, pinned to CPU 8, after the compiler build finished. Host-visible process checks found no competing builds around the measurement runs. Each sweep shuffles variant order with seed 437, excludes one warmup, and takes three samples per variant; every checksum passed. This is a small single-machine rejection test, not a representative architecture survey.

Ranges below are the minimum and maximum per-placement median cycles/row. Counters cover startup, construction, repeated calls and teardown; internal timers cover repeated calls. In the short scan, call/setup costs are substantial.

WorkloadOriginal alignment16 bytes32 bytes64 bytes
One-column fold, 8,192 rows2.04–4.072.04–3.062.04–3.053.04–3.11
Two-column filtered fold, 8,192 rows11.17–11.7210.87–12.7110.76–12.2410.79–10.91
Checked scalar range sum, 8,192 iterations3.01–3.022.01–3.022.01–3.023.01–3.02
One-column fold, 8 rows6.60–8.686.73–7.706.48–7.917.85–7.90

The one-column scan’s first sweep took 0.409–0.819 ns/row with original alignment, 0.409–0.618 with 32-byte alignment, and 0.609–0.624 with 64-byte alignment. A second complete sweep reproduced the same groups: 2.04–4.06 cycles/row originally, 2.04–3.06 at 32 bytes, and 3.04–3.06 at 64 bytes. Use these fresh within-run comparisons; earlier reports were measured at different CPU speeds. Across the first sweep’s alignments, retired instructions varied by fewer than 200 out of about 1.33 billion. Identical instruction counts are not identical throughput.

The scan function remains 294 bytes. At matching requested text addresses, 16-byte alignment adds 80 bytes to the linked text section, 32 adds 192–208, and 64 adds 416–464. Executable file size happens to remain unchanged because of existing segment padding. Across the other controls, text growth remains under 500 bytes; throughput tradeoffs, not this small size cost, reject the blanket change. Larger applications could accumulate more padding.

Interpretation and reproduction

Function alignment fixes the function’s starting boundary, not the hot loop’s boundary within it. The one-column loop has a condition at function offset 0xc6 and a body at 0xfd; aligning the function to 64 bytes does not align either block. Moving both blocks can improve or worsen instruction delivery. The exact CPU mechanism is still not isolated, and the probe moves other code as well. These results do not justify a benchmark-specific offset or NOP patch.

A further fix would need loop-aware backend work and broader validation; changing the global minimum alignment alone does not resolve this issue. That exceeds this bounded follow-up. No new user-facing flag is proposed.

The probe and all samples are in experiments/columnar-list/plenty/: probe_scan_layout.py and alignment-policy-measurements.json. Requires x86-64 Linux, GNU binutils, a C linker driver, and access to user-mode perf counters. After building the compiler, run the following sequentially without competing builds. Repeat the first command with a new output directory to check stability.

python3 -B experiments/columnar-list/plenty/probe_scan_layout.py target/release/plenty \
  --out /tmp/alignment-one --alignments 1 16 32 64
python3 -B experiments/columnar-list/plenty/probe_scan_layout.py target/release/plenty \
  --out /tmp/alignment-two --alignments 1 16 32 64 --kernel two --repeats 5000
python3 -B experiments/columnar-list/plenty/probe_scan_layout.py target/release/plenty \
  --out /tmp/alignment-scalar --alignments 1 16 32 64 --kernel scalar
python3 -B experiments/columnar-list/plenty/probe_scan_layout.py target/release/plenty \
  --out /tmp/alignment-tiny --alignments 1 16 32 64 --rows 8 --repeats 1000000