-
-
Notifications
You must be signed in to change notification settings - Fork 161
codegen: a float accumulator over masked reads earns the dense range clone — 17_loop_data_dependent at node parity (475→219ms) #9303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
proggeramlug
wants to merge
4
commits into
PerryTS:main
from
proggeramlug:fix/dense-accumulator-proof
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
85a398c
codegen: affine indices earn the range clone's hoisted receiver guard…
5556762
changelog: fragment for #9294
5e63da2
fix: rebase left the dense If arm calling pure_expr_collect with 4 args
5f81a68
codegen: a float accumulator over masked reads earns the dense range …
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| **A counted loop whose reads use an affine index now hoists its receiver guard | ||
| into the preheader** (#9253). `16_matrix_multiply` goes 100 ms → 69 ms against | ||
| node's 32 ms on an idle machine. | ||
|
|
||
| The interesting part was where the time was *not*. That loop spends 97% of its | ||
| time inside generated code with no runtime calls, so the cost was never a | ||
| missing inline or an unremoved helper. Per iteration, for both receivers, it | ||
| re-derived the pointer tag check, the handle-band check, the header | ||
| dereference, the `_reserved` flag tests and the two 16,000,000 length/capacity | ||
| sanity compares — for receivers that are loop-invariant parameters whose | ||
| headers cannot change inside the loop. LLVM cannot hoist that: the guard | ||
| reloads the header through a pointer it cannot prove unaliased, and the | ||
| incremental-barrier atomic read is a motion barrier. | ||
|
|
||
| The packed-f64 range tier already had everything required except a way to | ||
| describe `a[i * size + k]`, whose index has no compile-time window: it already | ||
| takes a loop-invariant local or parameter bound, emits one guard per receiver | ||
| AND-reduced into a single branch, and keeps its cached receivers GC-safe by | ||
| refreshing them on the back-edge poll. An access may now be *affine* — an | ||
| integer-producing expression over the loop counter and loop-invariant integer | ||
| locals — and such an access publishes a receiver-only fact: the entry guard | ||
| proves shape, raw-f64 packedness, integrity and the sanity bounds once in the | ||
| preheader, and each read pays one inline `icmp ult idx, len` with the fact's | ||
| existing side exit. | ||
|
|
||
| The index is materialised in i64 rather than i32, because `i * size` can exceed | ||
| i32 for a large matrix even when the final index is valid and an i32 | ||
| computation would wrap — turning an out-of-bounds access into an in-bounds one. | ||
| The bounds compare is unsigned, so a negative index reads as a huge unsigned | ||
| value and side-exits; no static non-negativity proof is needed, which matters | ||
| because `size` is a parameter with no callsite range summary and no static | ||
| window for the product is obtainable. The index must also mention the counter: | ||
| a wholly loop-invariant index like `a[0]` is affine by the grammar but has a | ||
| compile-time window, and admitting it here made the classic walker succeed and | ||
| silently stole those loops from the dense tier's masked path that serves them | ||
| better. | ||
|
|
||
| Reads only, in the classic mode only. Dense mode's loads carry no side exit and | ||
| so cannot take a per-read bounds check, and an affine store is rejected because | ||
| the side exit re-executes the iteration. | ||
|
|
||
| This does not reach parity. The residual is the per-read bounds check and index | ||
| materialisation, plus the `c[i * size + j]` store in the enclosing loop, which | ||
| stays generic for the reason above. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| **A float accumulator over masked reads now earns the dense range clone** | ||
| (`17_loop_data_dependent`: 475 ms → 219 ms against node's 220 ms on an idle | ||
| machine — parity, from 2.16×). | ||
|
|
||
| `sum = sum * x[i & 63] + x[(i * 7) & 63]` was rejected by the dense range tier | ||
| while `sum = sum * x[i & 63]` was admitted. The discriminator was the | ||
| accumulator's static numeric proof: `+` can be concatenation, so the | ||
| per-statement proof demands both operands numeric, and a reassigned | ||
| accumulator has no such proof — its own writes read the guarded array, whose | ||
| element proof only exists once the guard has run. A chicken-and-egg that `*` | ||
| never faces, because multiplication needs only the weaker inert fact. | ||
|
|
||
| The matcher now peels the accumulator: when the proof fails on the `LocalSet` | ||
| target of a self-accumulating write, it retries with the target treated as | ||
| numeric BY CONTRACT, records it pending, and then verifies every pending | ||
| local with the same collector the lowering runs — rejecting the whole dense | ||
| match (with its own named trace reasons) if the two disagree, so the clone can | ||
| never contain a dynamic `+` under facts that forbid one. The contract is | ||
| enforced at run time twice over: the clone's entry emits a genuine-double tag | ||
| check on the accumulator, and the dense entry guard validates the whole masked | ||
| window hole-free. A string-seeded accumulator and a string element both route | ||
| to the slow copy and produce node's concatenation, verified under forced | ||
| evacuation. | ||
|
|
||
| Along the way the accumulator walk's index leaf learned masked reads — and | ||
| fixed a match-arm reachability bug while doing so: `_ if offset_reads_inlined` | ||
| was a guarded catch-all, so any arm placed after it was unreachable whenever | ||
| the flag was set. Admitted masked-only single arrays now qualify for | ||
| accumulator admission (counter-bearing arrays keep priority; multiple arrays | ||
| still decline), and `MaskedWindowArrayFact` carries the admitted accumulators | ||
| so `is_numeric_expr` can see them while the clone lowers, mirroring the | ||
| string-window fact's field. |
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
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🌐 Web query:
LLVM LangRef: What are the overflow semantics of integer add, sub, and mul instructions when they do not have nsw or nuw flags?💡 Result:
In the LLVM Language Reference Manual, integer add, sub, and mul instructions that do not have the nsw (No Signed Wrap) or nuw (No Unsigned Wrap) flags behave according to standard two's complement modular arithmetic [1]. When an overflow occurs during these operations without these flags, the result simply wraps around, and no poison value is produced [2][3]. This wrapping behavior is the default, well-defined semantics for these instructions in the absence of wrap-restricting flags [1]. In contrast, if the nsw or nuw flags are present, they provide additional information to the compiler: - nuw: Indicates that unsigned overflow is a violation of the operation's contract. If unsigned overflow occurs, the result is a poison value [3]. - nsw: Indicates that signed overflow is a violation of the operation's contract. If signed overflow occurs, the result is a poison value [2][3]. For scenarios where overflow detection is required, LLVM provides specific arithmetic with overflow intrinsics (e.g., llvm.sadd.with.overflow., llvm.uadd.with.overflow.) that return both the result and a flag indicating whether an overflow occurred [4][5].
Citations:
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 7312
Add an overflow side exit for affine i64 arithmetic.
packed_f64_range_loop_index_is_affine_withrecursively accepts nestedAdd,Sub, andMulexpressions without an intermediate-value bound.emit_affine_index_i64emits wrapping LLVMi64arithmetic. A product such asi * 2 * ... * 2can wrap to zero, pass theicmp_ultceiling check, and reada[0]instead of treating the large JavaScript numeric key as an absent property. Use checked arithmetic with a side exit, or reject expressions that cannot be proven i64-safe.🤖 Prompt for AI Agents