Skip to content

zend_alloc: read the in-place limit of a small realloc from a table - #24331

Closed
marc-mabe wants to merge 1 commit into
php:masterfrom
marc-mabe:mm-realloc-keep-size
Closed

marc-mabe wants to merge 1 commit into
php:masterfrom
marc-mabe:mm-realloc-keep-size

Conversation

@marc-mabe

Copy link
Copy Markdown
Contributor

Whether reallocating a small block keeps it in place was decided by old_bin_num > 0 && size <= bin_data_size[old_bin_num - 1], which needs a test of the bin number, an index computation and a compare on every in-place reallocation.

Store the smallest size a block of each bin keeps in a table, bin_keep_size[], which is one more than the size of the bin below, and 0 for the first bin, so the check becomes a single load and compare. The behavior is unchanged.

As each entry comes from the row of the bin below, the table has one entry more than there are bins, and the last one is unused. This does not grow the binary: the table is aligned to at least 16 bytes, so 30 entries (120 bytes) would be padded to 128 bytes anyway, and the extra 4 bytes fit into that padding.

Mark the truncation as unexpected: a block reallocated to a smaller size usually stays in its bin. Without the hint, GCC merges the epilogue of the in-place return with another return path, which adds a branch to every in-place reallocation of a large run.

Benchmark arm64 amd64 (emulated) i386 (emulated) arm32 (emulated)
Zend/bench.php +0.000% (−0.0000%) −0.002% (−0.0000%) −0.000% (+0.0000%) −0.000% (+0.0000%)
Zend/bench.php JIT +0.000% (+0.0000%) −0.008% (−0.0000%) not run not run
Symfony Demo −0.023% (+0.0000%) −0.019% (+0.0000%) −0.003% (+0.0000%) not run
Symfony Demo JIT −0.025% (+0.0001%) −0.022% (−0.0000%) not run not run
WordPress 6.2 −0.009% (−0.0000%) −0.007% (−0.0000%) +0.001% (−0.0047%) not run
WordPress 6.2 JIT −0.007% (+0.0085%) −0.007% (+0.0010%) not run not run

Instructions per request (callgrind) vs. master (first run).
in parentheses master vs. master as a run-to-run control.

Whether reallocating a small block keeps it in place was decided by
old_bin_num > 0 && size <= bin_data_size[old_bin_num - 1], which needs
a test of the bin number, an index computation and a compare on every
in-place reallocation.

Store the smallest size a block of each bin keeps in a table,
bin_keep_size[], which is one more than the size of the bin below, and 0
for the first bin, so the check becomes a single load and compare. The
behavior is unchanged.

As each entry comes from the row of the bin below, the table has one
entry more than there are bins, and the last one is unused. This does
not grow the binary: the table is aligned to at least 16 bytes, so 30
entries (120 bytes) would be padded to 128 bytes anyway, and the extra
4 bytes fit into that padding.

Mark the truncation as unexpected: a block reallocated to a smaller size
usually stays in its bin. Without the hint, GCC merges the epilogue of
the in-place return with another return path, which adds a branch to
every in-place reallocation of a large run.
@iluuu1994

Copy link
Copy Markdown
Member

bin_data_size[old_bin_num] is accessed above and so bin_data_size[old_bin_num - 1] is very likely to be in cache. I think this should be benchmarked on real hardware to make sure there's no regression.

@ndossche

Copy link
Copy Markdown
Member

Right, now this patch adds another table that must be loaded from memory.
The instruction count from valgrind doesn't take into account how modern processors actually work.
This is also where micro benchmarks are deceptive: since micro benchmarks execute everything in a hot loop, it's likely that table will be cached too.
My gut feeling says this isn't a good change.

@marc-mabe

Copy link
Copy Markdown
Contributor Author

I'll run a real world bench with perf on real amd64/arm64. Just not sure when I can find the time for it.

@marc-mabe

Copy link
Copy Markdown
Contributor Author

@iluuu1994 @ndossche Seems you are right

Benchmark diff

  • Base: c65321c70f7d (HEAD) on mbennewitz, native, amd64, 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz -- 20261014-212054-mbennewitz-amd64-c65321c70f7d
  • Head: c9ae38d585d4 (mm-realloc-keep-size) on mbennewitz, native, amd64, 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz -- 20261014-212717-mbennewitz-amd64-c9ae38d585d4

Change from base to head; lower is better everywhere. The ± figure is the threshold a change must exceed to count as significant (bold): 2× the runs' combined noise for perf, a measured noise floor for callgrind and memory. ? means a single perf run gave no noise estimate.

perf: per request

Workload duration_time task-clock cycles instructions branches branch-misses cache-references cache-misses
Zend/bench.php +13.62% ±2.90 ❌ +13.72% ±2.93 ❌ +0.22% ±0.98 -0.01% ±0.02 -0.00% ±0.02 -1.98% ±6.95 -1.38% ±1.12 ✅ +15.67% ±5.67 ❌
Symfony Demo 2.2.3 +10.08% ±6.90 ❌ +10.06% ±6.93 ❌ -1.87% ±6.67 -3.50% ±7.12 -3.76% ±7.72 -3.50% ±4.89 -4.19% ±4.39 +19.10% ±28.46
Wordpress 6.2 +14.46% ±2.60 ❌ +15.90% ±2.56 ❌ -0.04% ±0.58 -0.01% ±0.00 ✅ -0.01% ±0.00 ✅ +0.77% ±0.87 +0.83% ±2.15 +10.86% ±3.89 ❌
Zend/bench.php JIT +7.99% ±4.29 ❌ +8.27% ±4.30 ❌ -0.69% ±1.02 -0.05% ±0.02 ✅ -0.04% ±0.03 ✅ -7.75% ±14.32 -1.82% ±1.15 ✅ +0.40% ±3.48
Symfony Demo 2.2.3 JIT +7.24% ±6.23 ❌ +7.30% ±6.38 ❌ -2.89% ±5.47 -6.26% ±6.36 -6.92% ±7.00 -4.53% ±4.64 -3.54% ±3.69 +26.31% ±6.67 ❌
Wordpress 6.2 JIT +13.61% ±1.93 ❌ +17.39% ±2.57 ❌ +0.81% ±0.38 ❌ +0.00% ±0.00 -0.01% ±0.00 ✅ -0.25% ±0.81 +1.41% ±0.78 ❌ +35.93% ±1.86 ❌
Absolute values
Workload Metric Base Head Change
Zend/bench.php duration_time 176.942 ms 201.032 ms +13.62% ±2.90 ❌
Zend/bench.php task-clock 176.040 ms 200.200 ms +13.72% ±2.93 ❌
Zend/bench.php cycles 708,719,746 710,291,282 +0.22% ±0.98
Zend/bench.php instructions 2,259,805,444 2,259,695,718 -0.01% ±0.02
Zend/bench.php branches 442,355,130 442,348,828 -0.00% ±0.02
Zend/bench.php branch-misses 1,044,447 1,023,809 -1.98% ±6.95
Zend/bench.php cache-references 1,361,591 1,342,848 -1.38% ±1.12 ✅
Zend/bench.php cache-misses 585,156 676,854 +15.67% ±5.67 ❌
Symfony Demo 2.2.3 duration_time 21.406 ms 23.565 ms +10.08% ±6.90 ❌
Symfony Demo 2.2.3 task-clock 21.381 ms 23.533 ms +10.06% ±6.93 ❌
Symfony Demo 2.2.3 cycles 87,099,227 85,468,201 -1.87% ±6.67
Symfony Demo 2.2.3 instructions 134,592,704 129,881,682 -3.50% ±7.12
Symfony Demo 2.2.3 branches 27,437,485 26,405,396 -3.76% ±7.72
Symfony Demo 2.2.3 branch-misses 809,366 781,015 -3.50% ±4.89
Symfony Demo 2.2.3 cache-references 1,413,155 1,353,980 -4.19% ±4.39
Symfony Demo 2.2.3 cache-misses 250,331 298,144 +19.10% ±28.46
Wordpress 6.2 duration_time 46.917 ms 53.701 ms +14.46% ±2.60 ❌
Wordpress 6.2 task-clock 40.351 ms 46.767 ms +15.90% ±2.56 ❌
Wordpress 6.2 cycles 169,922,069 169,853,412 -0.04% ±0.58
Wordpress 6.2 instructions 282,140,720 282,120,585 -0.01% ±0.00 ✅
Wordpress 6.2 branches 60,394,029 60,388,204 -0.01% ±0.00 ✅
Wordpress 6.2 branch-misses 1,596,233 1,608,569 +0.77% ±0.87
Wordpress 6.2 cache-references 2,423,071 2,443,101 +0.83% ±2.15
Wordpress 6.2 cache-misses 303,405 336,344 +10.86% ±3.89 ❌
Zend/bench.php JIT duration_time 65.041 ms 70.240 ms +7.99% ±4.29 ❌
Zend/bench.php JIT task-clock 64.080 ms 69.380 ms +8.27% ±4.30 ❌
Zend/bench.php JIT cycles 263,643,027 261,836,008 -0.69% ±1.02
Zend/bench.php JIT instructions 680,929,619 680,573,942 -0.05% ±0.02 ✅
Zend/bench.php JIT branches 139,498,295 139,437,647 -0.04% ±0.03 ✅
Zend/bench.php JIT branch-misses 811,338 748,420 -7.75% ±14.32
Zend/bench.php JIT cache-references 1,621,589 1,592,030 -1.82% ±1.15 ✅
Zend/bench.php JIT cache-misses 652,160 654,736 +0.40% ±3.48
Symfony Demo 2.2.3 JIT duration_time 23.543 ms 25.246 ms +7.24% ±6.23 ❌
Symfony Demo 2.2.3 JIT task-clock 23.495 ms 25.211 ms +7.30% ±6.38 ❌
Symfony Demo 2.2.3 JIT cycles 94,691,559 91,952,824 -2.89% ±5.47
Symfony Demo 2.2.3 JIT instructions 136,701,531 128,149,821 -6.26% ±6.36
Symfony Demo 2.2.3 JIT branches 27,343,893 25,451,587 -6.92% ±7.00
Symfony Demo 2.2.3 JIT branch-misses 922,847 881,032 -4.53% ±4.64
Symfony Demo 2.2.3 JIT cache-references 1,695,078 1,635,113 -3.54% ±3.69
Symfony Demo 2.2.3 JIT cache-misses 350,020 442,095 +26.31% ±6.67 ❌
Wordpress 6.2 JIT duration_time 45.605 ms 51.812 ms +13.61% ±1.93 ❌
Wordpress 6.2 JIT task-clock 38.856 ms 45.611 ms +17.39% ±2.57 ❌
Wordpress 6.2 JIT cycles 163,493,060 164,816,878 +0.81% ±0.38 ❌
Wordpress 6.2 JIT instructions 244,891,350 244,892,192 +0.00% ±0.00
Wordpress 6.2 JIT branches 50,577,685 50,573,582 -0.01% ±0.00 ✅
Wordpress 6.2 JIT branch-misses 1,569,591 1,565,693 -0.25% ±0.81
Wordpress 6.2 JIT cache-references 3,079,068 3,122,441 +1.41% ±0.78 ❌
Wordpress 6.2 JIT cache-misses 414,422 563,341 +35.93% ±1.86 ❌

@marc-mabe marc-mabe closed this Oct 14, 2026
@iluuu1994

Copy link
Copy Markdown
Member

Thanks very much for confirming!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants