Repository navigation
Conversation
At O2 the VIR pipeline gains four transformations and a normalization pass: - Post-SSA normalization reapplies the integer, bitwise and unary identities the builders use once parameter simplification and CSE expose equal operands, and lowers multiplication, unsigned division and remainder by a power of two to shifts and masks. Signed quotients are biased before the arithmetic shift so they still truncate toward zero. SCCP reruns when normalization changes the graph. - Affine-loop summarization replaces a single-entry, effect-free loop with a constant trip count and a recurrence a = A*a + B*i + C by its closed form, computed by repeated squaring modulo the type width. - Direct and additive tail recursion becomes a loop with an SSA accumulator; functions with stack objects, trapping operations or effects other than self calls are left alone. - Calls to small single-block pure leaves defined earlier in the file are cloned at the call site. --no-inline keeps the calls, so ABI and callee-graph tests still exercise them. DCE also does less work. uftrace showed pointer scheduling rescanning effects and use lists in every block; it now keeps a forward barrier cursor, skips blocks where nothing can move, and repairs positions only in blocks it changed. A self-compile on x86-64 takes 3.6 percent less time, with byte-identical output. On the native throughput workloads against GCC -O1, rng becomes 148.8x (its whole loop is summarized), recursion 1.19x, and the others stay within a few percent of their previous throughput. New tests cover the normalization identities, strength reduction, affine recurrences, tail recursion and inlining, including narrow types and wrapping arithmetic.
Comparing shecc's hot loops with GCC's on the native throughput workloads showed that the gaps came from register allocation and a few missed instruction forms, not from the IR: - Lowering claimed a register for every literal at its definition and held it until the last use, so three constants in sort's outer loop pushed live values out to their frame slots. An unpinned literal is now built where it is read, as read() already did after a flush. - x86-64 emits a single TEST for "if ((x & m) == 0)"; it previously computed the AND into a register before comparing. - Strength reduction derived a descending loop's start pointer from a fresh multiply. It now scales the index the way normalization rewrote the subscript and reuses the subscript's own address, offset by the bias. - A value computed in the block whose edge passes it to an if/else join may share the join parameter's register, so each arm updates the result in place. Literals are excluded: pinning an entry literal into a loop-carried register this way miscompiled a counted loop. - Counted pointer add-reductions with an even, compile-time trip count are unrolled by two into a second accumulator, halving the loop's dependency chain. The partial sums are combined ahead of every effect in the exit block. Local CSE also sizes each block's table from its order counter instead of counting the block's values twice. Cycles against GCC -O1 on a Zen 1 host, averaged over four code offsets: pointer goes from 1.00x to 1.48x, branch from 0.93x to 1.01x, sort from 1.00x to 1.05x, and recursion stays at 1.27x; calls and indexed remain bound by the same dependency chain as GCC's code. A self-compile runs 0.4 percent fewer instructions. Generated code changes only through the transformations above.
When a load is elided into src0_override, the store writes the override register, yet the frame mirror recorded the IR source operand, which never received the value. A later reload of that slot was then satisfied from the wrong register. Record, and test for redundancy against, the register the store actually writes. The new vir-store-override fixture returned 1 before this change.
VIR now inlines direct calls to leaf functions defined earlier in the translation unit. The callee's reachable blocks are cloned in reverse post-order into the caller, the call block is split at the call, and returns jump to a rest block whose parameter carries the result. A static callee is copied when it has at most 40 values; an extern one only at 6 values, or 16 when the call sits on a cycle of the caller's CFG. Any callee whose calls are all inlined leaves the image, but linkage predicts how many callers keep it anyway: on a self-compile, a uniform limit and dropping the static one both made the generated compiler larger and slower. A caller absorbs at most 400 cloned values. Callees that take their address, return an aggregate, call out, or touch the stack are left alone, so recursion stays a call. On a frozen self-compile this costs 3.9 percent more compiler instructions and 2.5 percent more code, and the generated compiler runs 2.6 percent fewer instructions; the calls workload drops from 40M to 32M instructions. --no-inline is removed, since --no-opt already disables it. Fixtures that check a call survives now define their helper after the caller, and unit tests check inlined results by value. Closes #285
The README accumulated dated measurement sections and a stale ARM64 table. Replace them with the newest x86-64 run, a command to reproduce it, and how to compare two compilers.
A token carried two filename pointers in its location, the physical path and the logical name #line may change. Every token of a file now points at one shared record holding both, and #line points the tokens after it at a new record, so a token shrinks from 56 to 48 bytes on a 64-bit host. error_at() now finds the quoted source by the physical path, which is where the buffer is stored and what the location's offset indexes, and still reports the logical name. Before, a #line naming another file quoted that file's text, or none.
The parser's generated temporaries, over a hundred thousand var_t of 192 bytes on a self-compile, stayed in BLOCK_ARENA until the end, though nothing reads them once a function's graph is built. Allocate those of a function in TEMP_VAR_ARENA instead and rewind it after the function; temporaries at file scope still serve the global initializer and stay. A temporary is now made by a require_temp_*() helper, which also keeps it off the scope's lookup list from the start instead of pushing and popping it. Together with the smaller tokens, a self-compile peaks at 58.9 MB instead of 78.8 MB, with 24 percent fewer page faults and slightly fewer instructions; output is byte-identical.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This brings shecc level with or ahead of GCC -O1 on most native throughput workloads on x86-64. The first commit adds VIR optimizations at O2: post-SSA integer and bitwise normalization with power-of-two division lowering, closed-form summarization of affine counted loops, and additive tail-recursion elimination. It also makes DCE's pointer scheduling cheaper, guided by uftrace. The second commit closes what remained in the generated code: literals are no longer held in registers from their definition, x86-64 tests a mask with TEST instead of AND plus compare, strength reduction reuses the subscript's address, if/else arms update a shared join register, and even-count pointer add-reductions are unrolled into two accumulators.
The inlining commit implements #285: direct calls to leaf functions defined earlier are cloned into the caller, up to 40 values for a static callee, and 6 values (16 inside a loop) for an extern one, within a 400-value budget per caller; linkage stands in for how many callers keep a copy, and a uniform limit measured larger and slower. Recursive calls stay calls. On a frozen self-compile it costs 3.9 percent more compiler instructions and 2.5 percent more code, and the generated compiler runs 2.6 percent fewer instructions; the calls workload drops from 40M to 32M instructions.
--no-inlinewas dropped since--no-optalready disables it. Enabling it exposed an x86-64 frame-mirror bug, where a store through an override register recorded the wrong register as mirroring the slot; that fix and its regression test are a separate commit.Two commits cut the compiler's own memory, guided by uftrace and perf, which showed a flat CPU profile and a quarter of a self-compile spent in page faults. The parser's generated temporaries, over a hundred thousand 192-byte var_t that stayed allocated to the end, now live in an arena rewound after each function, and a token's location holds one pointer to a shared file record instead of two filename pointers, shrinking tokens from 56 to 48 bytes. A self-compile peaks at 58.9 MB instead of 78.8 MB, with 24 percent fewer page faults and slightly fewer instructions; output is byte-identical, and a sanitizer build that frees every temporary block after each function found nothing on 394 programs. error_at() also now quotes the physical file under #line.
Throughput against GCC 14.2 -O1 (
-fwrapv) on a Threadripper 2990WX, nine rotating rounds of eight executions pinned to CPU 63, wall time: pointer 1.41x, recursion 1.18x, sort 1.11x, indexed 1.00x, calls 1.00x, branch 0.97x, and rng 150x because its loop is summarized away.Verified with
make bootstrap(stage 1/stage 2 byte identity) on arm, arm64, riscv and x64,make check,make check-sanitizerandmake check-all-targets, all passing. Refactors were checked by comparing generated code for 351 test programs byte for byte against the compiler before each change.Left out: a Linux-only huge-page heap setup, which cut a self-compile by 8 percent but is not portable, and a loop-depth weighting for register pinning, which miscompiled
pp_stringifyin the self-hosted compiler.Closes #285