Chapter 16 · 1–22 October

The month after 100%

v5.1.0 passed every file of Roast on 30 September. Six releases followed in October, and every one of them kept every file passing, through a Roast that grew a file along the way. With the count at its ceiling, the work turned to three things: what 100% had cost, the wrong answers a passing suite had not caught, and the places a program can run.

392 → 14 msfib interpreted, v5.1.0 to v5.2.0: subs compiled to integer kernels on their first call
1,425 / 1,425Roast files from v5.3.0 on, after Unicode 18 added one
0 → 319,078of 320,000 objects in reference cycles freed by one loop, in v5.5.0
18 of 18kernels where v5.5.0 takes less time than Rakudo 2026.09, interpreted and compiled
The turnThe tree-walker had been made faster by shortening its path (chapter 15). v5.2.0 took the hottest code off that path altogether. A sub whose body is closed integer arithmetic is compiled on its first call into int64 nodes and runs there. When the kernel cannot answer exactly (an argument that is not a plain Int, an overflow, a zero divisor), the call re-runs the ordinary way from the start, so results and errors do not change. Loops over plain Int and Str variables got the same treatment.
fib: integer kernels, from v5.2.0.
rats: kernels that take Rats, from v5.3.0.

Both charts come from the one Apple M3 of chapter 14, so they end at v5.3.0, the last release it timed.

Kernels

Against the v5.1.0 release binary in the same sitting, v5.2.0's interpreter was faster on every kernel: fib −97%, loopsum and streq −93%, mainwhen −92%. It now took less time than Rakudo 2026.09 on fifteen of seventeen. --exe gives each kernel sub an int64 twin in the C++ it emits, and RAKUPP_NO_KERNELS=1 turns both off.

v5.3.0 let kernels take Nums, Rats, ** and pure methods. A double travels in the integer frame as its bits, and a Rat is a numerator slot and a denominator slot with 128-bit arithmetic. rats went from 176 ms to 25. Loop kernels began to use arrays and hashes in place, with an undo log that takes the writes back if the loop bails. On arraypush, rats and intcat the interpreter became faster than the compiled binary.

v5.5.0 found that ordinary code had been missing all of it. A decimal literal is a Rat, and a Rat beside a Num kept a loop off the kernels, so code written the way most people write it ran about 60 times slower than the same line with e0 literals. $x = $x * 1.0000001 + 0.000000001 went from 3,418 instructions an iteration to 57.

What it cost, and how that was shipped

v5.3.0 made method calls slower, by 4.5 to 9.5% on the call kernels. Retired instructions grew 6 to 7% on the worst three, so it was work added, not code layout. A bisect found no single commit: the cost had built up over about 120 commits, a percent or two at a time. The release shipped it measured rather than fixed, with the three per-call costs the profile named, and v5.4.0 removed one of them. v5.5.0 put the call path another 4 to 9% behind v5.4.0. Its notes say this was accepted for the release, not fixed, and that the costs the commits admitted when they landed do not add up to it.

The release gate, perf-guard, compares every kernel with a recorded baseline. At v5.2.0 it could not record one, for a new reason: fib, loopsum and three other kernels now ran in 5 to 13 ms, about 4 of which was process startup, so their runs spread wider than the 5% the gate enforces. v5.2.1 sized its six fastest kernels past startup and made v5.2.0's the baseline. That is still the baseline. v5.3.0's attempt to record a new one refused three times on noise and was not forced, and the M4 refused one at v5.5.0.

A Roast that grew, and the answers it does not check

On 9 October Roast carried Unicode 18.0's generated tests and one new file, S15-nfg/GraphemeBreakTest-4.t, for 1,425 files in all. The v5.2.1 binary passes 1,405 of them; the 20 it fails are the new Unicode tables. v5.3.0 regenerated every UCD and UCA table, added UAX #29's new GB9c rule, and passed all 1,425 files and all 219,207 tests with skip and todo left out. Every release since has done the same.

A suite at 100% can still miss a wrong answer. v5.5.0 added oracle gates, which check Raku++ against something other than Roast: its DEFLATE against libz, Int and Rat arithmetic against Python in about 580,000 cases, and every line of Unicode's own NormalizationTest, GraphemeBreakTest and CollationTest. The release's notes give a whole section to silent wrong answers, most of them found by those gates' first runs. Among them:

Linear again

v5.3.0 made building a string linear. ~= appends in place for every value type, and a long string may be a view of a shared buffer, so prepending and .substr loops no longer copy the whole string. intcat, two million Int appends into one string, took 50 ms. v5.2.1 had not finished it in a minute.

The same release made loading quadratic. It asked the whole file a question for every word after a term, so 3,000 one-line subs loaded in 176 ms against v5.2.1's 22, and 30,000 took 16 seconds. v5.4.0 brought that back to 22 ms and added a gate so it could not happen quietly again: t/scaling times 171 shapes of program at n and at 8n, and fails any that grows more than 24 times for 8 times the size. It found most of the run-time quadratics that v5.4.0 then fixed. Twenty thousand appends of non-ASCII text had taken 5.25 s; eighty thousand now take 2 ms. A 2,705-line grammar over accented text went from 50.66 s to 0.06.

Memory, and objects in cycles

A value went from 128 bytes to 80 in v5.2.0 (chapter 10 tells how it got from 344 to 128). With the rest of that release's memory work, a million-element array of Ints went from 258 MB to 85, and 200k objects with two attributes from 946 MB to 133. In v5.5.0 a closure went from 704 bytes to 88, because everything about its code moved into a block that every closure of it shares.

Reference counting cannot free objects that refer to each other, and until v5.5.0 nothing else did. The loop in issue #151 freed none of its 320,000 objects and grew from 238 MB to 1,882. v5.5.0 keeps reference counting and adds a backup cycle collector, which finds groups whose counts only explain each other and breaks them, at scope exits and compiled loop iterations. The same loop now frees 319,078 objects and grows from 10 MB to 11; Rakudo 2026.09 frees 319,252. It also ended a leak in every call of a routine that declares a my sub or keeps a block in a variable.

Where it runs

A second machine

Every benchmark on these pages until 13 October ran on one Apple M3. For v5.5.0 it was out of reach, and an Apple M4 stood in. The M4 is not a uniformly faster M3. On builds both machines timed, it takes 0.75 to 1.03 times the M3's time depending on the kernel, about 0.9 at the median, so no single factor turns one machine's milliseconds into the other's. Ratios carry across. The sitting re-timed two builds the M3 had timed, to tie the two machines together, and the dashboard draws the M4's readings as lines of their own.

On the M4, v5.5.0 takes less time than Rakudo 2026.09 on all eighteen kernels, interpreted and compiled. The closest are multiwhere (1.09× interpreted, 1.08× compiled) and objects (1.2× and 1.18×). On multiwhere that lead is the machine's, not the release's: on the M3, v5.4.0 had trailed Rakudo there by 7%.

Left open

v5.5.0's notes end with what is still behind. Text against Perl is the widest gap: 94× on .trans, 19× on output, 14× on regex captures. Rakudo is still 5 to 10 times faster at binding an array element. map and grep are still eager outside .first and .head. The rest of the call-path cost has not been found, and regex lost 6 to 7% over the cycle in steps of about 1%.