|
sdk-core v2026.11.05
PS2 SDK - core tier (build-critical EE/IOP modules)
|
Directories | |
| src | |
Files | |
| gen_inflate_data.py | |
| shortloop_scan.py | |
tests/bench.sh builds tests/bench with GCC and LLVM at -O1, -O2, -O3 and -Os, runs each build in PCSX2 headless, and prints cycles (COP0 Count) and function size for each kernel with LLVM's ratio to GCC. PCSX2 does not model the memory system, FPU latencies or cache misses of a real console, so the speed figures below come from the hardware rig; run the ELFs in bench/build-<toolchain>-<level>/out/bin there for hardware numbers.
| Kernel | Covers |
|---|---|
| memcpy_mmi, memset_mmi | 128-bit lq/sq copy and fill of 64 KiB |
| memcpy_libc, memcpy_loop | libc memcpy and a word loop over 64 KiB |
| packet_build | GIF-style packet of 2000 vertices (RGBAQ, ST, XYZ2) |
| decompress | LZ77 decoder over a 16 KiB stream |
| inflate | Table-driven raw deflate decoder: 24 KiB zlib level 9 stream to 64 KiB of text and binary data |
| physics | 1024 particles for 30 frames: float integrate, bounce, sqrtf |
| raster | 200 edge-function triangles into a 128x128 buffer |
Every kernel's checksum is identical on both toolchains and on PCSX2 and hardware (physics 46fb9806, raster c8cb2e87, packet_build 67522a17, inflate 3f62b2bb). gen_inflate_data.py writes src/inflate_data.h; the checksum is that of the uncompressed data.
Each kernel runs three times with interrupts disabled (DIntr/EIntr around the timed call) and reports the fastest run. physics and raster reset their input before each run, so all three do the same work.
raster also varies between boots of the same ELF, in steps: LLVM -Os gives 7.63M or 7.89M cycles, GCC -Os 9.37M or 9.62M, GCC -O2 6.85M to 7.16M. The three runs inside one boot always agree, so the difference is state that lasts for the whole boot; its cause is not known. The figures below are the minimum over two boots. The other kernels repeat within 0.3% between boots.
Compiler: ps2/llvm r5900-ee with ps2/llvm#15 (R5900 scheduling model and FP cost changes) and ps2/llvm#16 (integer movz/movn). Measured 2026-10-15.
| Kernel | -O1 | -O2 | -O3 | -Os |
|---|---|---|---|---|
| memcpy_mmi | 1.00 | 1.00 | 1.00 | 0.99 |
| memcpy_libc | 1.00 | 0.95 | 0.95 | 1.00 |
| memcpy_loop | 0.65 | 0.95 | 0.95 | 1.00 |
| memset_mmi | 1.01 | 1.01 | 1.01 | 1.01 |
| packet_build | 0.84 | 0.94 | 0.95 | 0.44 |
| decompress | 0.96 | 0.79 | 0.77 | 0.79 |
| inflate | 0.92 | 0.98 | 3.14 | 0.84 |
| physics | 1.52 | 1.15 | 2.05 | 1.04 |
| raster | 1.06 | 1.18 | 1.01 | 0.83 |
Adding inflate moved the other kernels' code and data. GCC -Os packet_build and decompress changed by 3% and 7%, and raster at -O3 went from 0.98 to 1.01 and at -Os from 0.87 to 0.83.
inflate counts bench_inflate and bench_inflate_table, and every clone of either (GCC -O3 makes three bench_inflate_table clones).
| Kernel | -O1 | -O2 | -O3 | -Os |
|---|---|---|---|---|
| memcpy_mmi | 1.11 | 1.05 | 1.05 | 0.95 |
| memcpy_libc | 1.57 | 0.58 | 0.58 | 1.00 |
| memcpy_loop | 0.85 | 0.58 | 0.58 | 1.00 |
| memset_mmi | 0.95 | 1.00 | 1.00 | 1.00 |
| packet_build | 0.89 | 0.98 | 0.98 | 0.66 |
| decompress | 0.86 | 0.75 | 0.37 | 0.79 |
| inflate | 1.01 | 1.00 | 0.64 | 1.13 |
| physics | 0.78 | 1.02 | 0.75 | 0.75 |
| raster | 1.09 | 0.81 | 0.89 | 1.19 |
| all kernels | 0.98 | 0.95 | 0.65 | 1.04 |
| Padded physics | GCC | LLVM | LLVM/GCC |
|---|---|---|---|
| -O1 | 2.64M | 2.20M | 0.83 |
| -O2 | 1.93M | 2.07M | 1.07 |
| -O3 | 1.50M | 2.07M | 1.38 |
What remains is code: at -O2 LLVM computes px*px + pz*pz after the bounce branch, so a nop sits between the second c.lt.s and bc1f; GCC computes it before the first branch. At -O3 GCC specializes the function for the constant frame count and handles two frames per particle pass.
Over all kernels LLVM is smaller than GCC at -O1, -O2 and -O3.
shortloop_scan.py <objdump> <elf or .a>... lists loops of six instructions or fewer, with one conditional branch at the end, whose delay slot is not a nop, which is the case the R5900 erratum breaks. Over the bench ELFs at every level and every library ps2build installs, it finds none in EE code for either toolchain; the only hits are in IOP libraries (libbdm, libspu2, libsnd2), where the erratum does not apply. With -mattr=-fix-r5900 it flags the loop in llvm/test/CodeGen/Mips/r5900-short-loop.ll.
LLVM's assembler never moves an instruction into a delay slot, so assembly written without .set noreorder is safe; GNU as -march=r5900 pads the same loop with a nop.
fpu-check prints 22 float operations on edge values, each computed at run time from volatile inputs and again from constants the compiler folds. On the console, GCC and LLVM give the same run-time results, except:
The folded results differ from the console for both compilers. LLVM folds with IEEE rules: 1.0f / 0.0f and overflows fold to 0x7f800000, inexact sums round to nearest, where add.s truncates. GCC folds with a round-towards-zero, no-infinity model, which matches add.s and the clamping but is off by 1 to 3 units in the last place for 1.0f / 3.0f and 1.1f * 1.1f. Both fold underflows to denormals, which the console flushes to zero. These need IEEE-independent float folding to change and are documented as exceptions.
R5900 unroll-and-jam and partial unrolling closed the physics, raster and packet_build gaps, but they needed changes to generic LLVM passes and made -O2 code 1.70x GCC's size, so they are not in the compiler.