Repository navigation
Conversation
|
Marking as draft so I can figure out how many of these checks LLVM was already able to remove via it's own constant folding logic. |
|
Update with a measurement of how much of this LLVM could already do on its own. The PR description says about 10% of slicing sites take the new path. That counted every site where the check is skipped, including ones where LLVM would have removed the check anyway. To separate the two I built a throwaway experiment, not part of this PR. It emits the high > max check of s[i:] as its own branch to a marker panic function, with nothing skipped. After optimization I count the markers that are still in the IR. Those are the checks LLVM cannot remove by itself, so those are the ones this PR actually removes. I compiled the test binaries of about 100 Go repos plus the standard library and runtime, for native and wasip1. Nothing was run. So LLVM already removes about half of the new path checks. The PR removes the other half. On wasip1 that is 2502 checks, about 5% of the roughly 50,000 slicing sites in the same set. About 2.5% of all sites lose a check that was inside a loop. The corpus repos and the standard library behave similarly. 46% of corpus sites and 59% of std and runtime sites on wasip1 keep the check without this change. Caveats. Survivors are counted once per function name across binaries, and a site that is inlined into several callers can count more than once, so the survival rate is approximate and may be a little high. Loops are detected as a cycle in the function's control flow graph. Four corpus jobs do not build and are missing from the numbers. I have not measured cortex-m. The earlier benchmark numbers (827 us to 98 us for a loop doing s = s[4:]) come from a case where the check stayed in the loop, which is the kind of site counted in the loops row above. |
a1125d6 to
b4f9904
Compare
|
Follow-up with measurements on a Cortex-M0+, which the earlier comment said I had not done. On a Seeeduino XIAO (SAMD21, 48 MHz, -opt=s) I counted cycles with SysTick for small kernels that re-slice a 1 KB buffer. The counts are exactly repeatable and do not depend on code layout. The gain matches the drop in instructions in each loop. -opt=z does not build with my local LLVM sources, so I measured -opt=s. The 827 us to 98 us case above is at -opt=2. At -opt=z the same loop is about 8% faster. |
davecheney
left a comment
There was a problem hiding this comment.
Ok, correct review on the correct PR this time
| Program | Target | -opt=1 | -opt=z | -opt=2 |
|---|---|---|---|---|
pdp11 pidp11-bootmon |
waveshare-rp2350-pizero | -656 | -576 | -448 |
corewar corewar-badge |
gopher-badge | -464 | -336 | -288 |
pico8 p8bench |
waveshare-rp2350-zero | -112 | -240 | +16 |
pico8 p8demo |
waveshare-rp2350-zero | -256 | -304 | -112 |
At -opt=z most of the reduction is in functions whose IR lost a compare or a whole slice check: 446 of 578 bytes for pdp11, 294 of 340 for corewar. The largest are corewar/ui.Game.pick (-280) and fmt.fmtFloat (-248 on RP2350). fmt.fmtFloat loses the same 4 compares on RP2040 but gets 52 bytes larger there. The p8bench growth at -opt=2 is machine/usb/cdc.USBCDC.Write (+34), where LLVM restructures the loop.
|
@dgryski please rebase aagainst latest dev and resolve merge conflicts. Thank you. |
When slicing a slice without a high or max index, the high bound is the length and the max bound is the capacity of the same slice. The check high > max is then always false because len <= cap. LLVM often cannot know this, so s[i:] can keep an extra comparison that blocks loop optimizations. Do not emit high > max in that case. Also do not emit max > cap when max is the capacity itself, as in s[a:b]. The remaining checks are unchanged. On a synthetic loop that consumes a byte slice with s = s[4:] this is 8x faster at -opt=2 and 8% faster at -opt=z, as the median over 6 code layouts. Code size on the testdata programs changes by less than 0.4% in either direction, measured on wasip1. Add compiler tests for the emitted checks and runtime tests for the panic cases. Update the expected binary sizes.
b4f9904 to
46a7687
Compare
Use the sizes measured in CI for pico2. The row for size-contains is new on dev and the slice change affects it.
|
PTAL |
When slicing a slice without a high or max index, as in s[i:], the high bound is the length and the max bound is the capacity of the same slice. The check high > max is then always false because len <= cap, but LLVM often cannot know that. The extra comparison can stay in the IR and block loop optimizations.
This change does not emit that check. It also does not emit max > cap when max is the capacity itself, as in s[a:b]. LLVM already folded the second case, so it only trims the IR. All other checks are unchanged.
Results
Measured on darwin/arm64 and wasip1. Timings are medians over 6 code layouts, built with different amounts of unused code, to reduce alignment noise.
A loop that consumes a byte slice with s = s[4:] is 8x faster at -opt=2 and about 8% faster at -opt=z. A loop that mixes s[i:] and x[1:] is 11% faster at -opt=2 and 21% faster at -opt=z. utf8.Valid on mixed text is 7% faster natively and 22% faster on wasip1. base64 decoding is 3 to 4% faster natively.
On a Seeeduino XIAO (SAMD21, Cortex-M0+ at 48 MHz, -opt=s) I counted cycles with SysTick for loops that re-slice a 1 KB buffer. They take 11.0%, 15.6%, 2.8% and 1.4% fewer cycles in the four loops that changed. One other re-slicing loop and two control loops are unchanged. The cycle counts are exactly repeatable and do not depend on code layout.
Most other benchmarks I tried (encoding, image/draw, compression) are within a few percent either way. Some small slicing loops are 6 to 9% slower. I checked one of them and its machine code is identical, so that is code placement and not this change.
Code size on the testdata programs for wasip1 changes by less than 0.4% in either direction. The largest increase was json at -opt=2 with 992 bytes (0.1%). It also shrinks json by about 1.3 KB at -opt=s and 1.5 KB at -opt=z.
Roughly 10% of slicing sites take the new path. LLVM already removes the check at about half of those. The other half, about 5% of all slicing sites, lose a check they would otherwise keep, and about 2.5% of all sites lose one inside a loop. I measured this on wasip1 and native with a corpus of about 100 Go repos plus the standard library, by building the test binaries without running them.
Testing