# Prismio Research: full text Generated from the same files as https://research.prismio.org. Each entry starts at a level-one heading. # Closing the gap to C on wrapping integer loops - URL: https://research.prismio.org/papers/wrapping-integers-and-tight-loops - Authors: Saksham Jaiswal - Published: 2026-10-08; updated 2026-10-22 - Measured on: Prismio 0.1.0 - Status: published - Keywords: compiler, llvm, integer-overflow, loops, performance, benchmarks - License: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0) ## Abstract Prismio's Int is a signed 32-bit integer that wraps on overflow by definition, whereas signed overflow in C is undefined and lets LLVM rewrite arithmetic and widen loop counters freely. On an instruction-counting freestanding suite, four tight integer workloads ran 2 to 12 percent behind C for that reason alone. We attribute the gap by marking one operation at a time as non-wrapping, find three distinct shapes behind it, and build four compiler analyses that prove the facts LLVM needs while leaving wraparound defined. After the change four of twenty workloads beat C, sixteen are level and none are behind, with a geometric mean of 0.95 times C. We also report ten alternatives that were measured, including undefined overflow by default, which moved the hosted suite's geometric mean by 0.9986 times and so was rejected as a language change with no measurable benefit there. ## Key findings 1. Before the change, quicksort, knapsack and edit_distance ran 5 to 12 percent behind C and heapsort 2 percent, measured in executed instructions on AArch64. 2. The gap has three shapes: a loop counter that cannot be widened, a data-dependent recurrence in a loop condition, and arithmetic on array contents. 3. Knapsack went from 5,690,512 to 3,139,600 instructions, 0.613 times C, from one fact: the subscript's left operand is non-negative. 4. Making every signed add, subtract and multiply non-wrapping moved the 62-workload hosted suite's geometric mean by 0.9986 times, inside noise. 5. Marking the loop counter alone made knapsack worse, at 6,258,704 instructions, because the subscript still wraps. Prismio's `Int` is signed 32-bit and **wraps on overflow by definition**. C's signed overflow is undefined, so LLVM may assume a C `int` never wraps. It can rewrite `min(a + 1, b + 1)` as `min(a, b) + 1`, and widen an `int` loop index to 64 bits so it addresses memory without a sign extension. For Prismio it may do neither unless the frontend proves the arithmetic cannot wrap and says so with LLVM's `nsw` flag. The reasons `Int` is 32 bits at all are in [Why Prismio's Int is 32 bits and wraps](/notes/int-is-32-bit-and-wraps). On the freestanding suite, which counts instructions exactly, that was the whole of the remaining gap to C. The investigation took place on 2026-10-08. Wraparound stays the language's default throughout; the question was how much of C's speed a compiler can recover without undefined overflow. ## Where the instructions went Each row was attributed by adding `nsw` to one operation at a time in the compiler's own IR and counting again. The counts are instructions executed on aarch64 freestanding with NEON off. | workload | Prismio | C | all signed `+ - *` `nsw` | what carries the whole difference | | --- | ---: | ---: | ---: | --- | | `quicksort` | 11,304,368 | 10,096,384 | 9,991,296 | `i = i + 1` and `j = j - 1` in the two scans | | `knapsack` | 5,690,512 | 5,122,624 | 5,122,720 | both `c - 1` and the subscript `c - w`; `c - 1` alone is worse, at 6,258,704 | | `edit_distance` | 5,900,624 | 5,596,832 | 5,540,608 | the value adds `prev[j] + 1` and `cur[j - 1] + 1`; the index arithmetic contributes nothing | | `heapsort` | 10,907,456 | 10,702,272 | 10,555,632 | `root * 2 + 1` in the loop condition of `sift` | The four rows are three shapes. - **An index counter that cannot be widened** (`quicksort`, `knapsack`). Each step addresses through a 32-bit register with a sign extension, and quicksort also keeps a `mov` of the pre-increment value, so a scan loop runs 5 instructions per element against C's 4. - **A data-dependent recurrence in a loop condition** (`heapsort`). `root = pick` is not an induction variable, so no counter analysis applies. - **Arithmetic on array contents** (`edit_distance`). `min(a + 1, b + 1)` becomes `min(a, b) + 1` only if neither add can wrap, and what bounds `a` and `b` is the history of the arrays, not any index. C built with wrapping defined reproduces Prismio's counts on three rows and lands on 6,258,608 on knapsack, the same as `c - 1` marked alone. It gets there because clang marks C loops as guaranteed to make progress, so LLVM may widen the counter. The subscript `c - w` is still computed in 32 bits. ## What was built Four analyses, all in the compiler, none changing what a program means. 1. **A range guard for loops over sized arrays.** An `Array` access is never checked, so there was nothing for a guard to remove, and the range analysis used to run only for loops with a list guard. It now serves an innermost loop's sized-array accesses with the type's `N` as the length. Knapsack's guard is `w >= 0`, read off `best[c - w]` in `[0, 1501)` over `c` in `[w, 1500]`. From that one fact LLVM proves the loop cannot wrap and writes the `nsw` itself. 2. **Facts an unchecked access implies.** The language requires an index to be in range, so an access that ran had its index in `[0, len)`, and every length fits `Int`. That marks any add or subtract whose operands the facts bound, which covers quicksort's scans where no guard can reach, and any subscript whose left operand is known non-negative. A counter's update is marked only when every subscript in its loop that uses it is, because the knapsack regression is exactly an update marked without its subscript. 3. **Counted loops.** A `while` whose condition bounds a counter and whose last statement steps it by a literal bounds the counter in the body. With a fact on where the counter starts, the walk also counts the iterations. 4. **What a local array holds.** A local array that nothing but a subscript names keeps a bound on its elements over one lifetime. Edit distance's rows start at zero and take 600 × 600 stores of at most one more than an element, so they stay in `[0, 360600]`, which marks the value adds. The two adds alone predicted 5,540,608 from hand-edited IR, and the analysis gives 5,540,624. ## Results | instructions | before | after | C | after / C | | --- | ---: | ---: | ---: | ---: | | `quicksort` | 11,304,368 | **9,991,296** | 10,096,384 | 0.990 | | `knapsack` | 5,690,512 | **3,139,600** | 5,122,624 | **0.613** | | `sha256` | 9,018,112 | **8,634,112** | 9,546,064 | 0.904 | | `edit_distance` | 5,900,624 | **5,540,624** | 5,596,832 | 0.990 | | `heapsort` | 10,907,456 | 10,907,440 | 10,702,272 | 1.019 | Against C that is 4 workloads fewer instructions, 16 level and none more, with a geometric mean of 0.95 times. The first results read 3, 14 and 3, with a mean of 0.99 times. - **Each row has one cause.** Quicksort's gain is entirely the access facts: switching the mark source off puts it back at 11,304,368. Knapsack's is entirely the guard, and stays at 3.14 million with both mark sources off. Edit distance's is entirely the content bound. - **`sha256` was not a target and its gain is not a proof.** With both mark sources off it still reads 8,634,112. What changed is two `mov`s per round, 64 rounds times 3,000 blocks, which is 384,000. It is code shape that happened to fall the right way. `heapsort` shows the other side: with only the access facts off it reads 11,108,928, because an inlining decision near its threshold flipped. - **Everything else is unchanged.** The other 15 rows moved by at most one 16-instruction tick, with every checksum equal, and the text section grew 200 bytes (1.3%). - **The hosted suite is untouched.** Against a baseline built from the same tree, its 140 functions are instruction-identical. - **The compiler changes a little.** Its own IR gains 41 `nsw` marks, from 369 to 410. Emitting it costs 0.9% more instructions, a median of 10.53 billion against 10.43 over five runs. Two compiler generations still reach a byte-identical fixpoint. ## What every alternative measured Each alternative was run in a scratch compiler or by hand-editing the pre-pipeline IR. Only the array-content analysis is in the tree. | alternative | rows recovered | verdict | | --- | --- | --- | | Signed overflow undefined by default | all four | **Rejected.** It changes the language, and over 62 hosted workloads the geometric mean moved 0.9986 times | | An opt-in build mode with the same marks, trapping in debug | all four | **Deferred.** A semantics mode, and the content bound recovered `edit_distance` without one | | Version knapsack's loop on `w != Int.MIN` | none: 6,259,472, worse than nothing | **Rejected.** A widened counter with an unproved subscript costs a truncation and an extension per access | | Overflow yields an unspecified value, with a `freeze` before every use | `edit_distance` only; heapsort worse, at 11,059,840 | **Rejected.** A counter must be frozen before its first use, which hides it from scalar evolution | | Assume every loop terminates | knapsack's counter only, the 6.26M shape | **Rejected.** It makes a side-effect-free infinite loop undefined, a trap in freestanding code | | An `assume` after every access | quicksort to 10,124,800; knapsack unchanged | **Rejected.** The counter's bound is derived but not the subscript's | | The access-implied invariant as one `assume` in the loop preheader | knapsack 3,139,216 | **Not built.** It widens what a latent out-of-range bug can do | | Array-content range analysis | `edit_distance`, 5,540,624 | **Built.** Analysis 4 above | Heapsort's recipe is known and measured but not built: a guard on `start` and `end` plus `root >= 0`, carried through the recurrence, makes `root * 2 + 1` provably unable to wrap and brings the row to 10,555,648. It would be a fifth analysis for a row already inside the suite's ±2% band, and it is the one row still behind C. ## Method The first check needs no IR. Run the two rows, then again with one mechanism's marks switched off: ```bash python3 benchmarks/run.py --suite freestanding --only quicksort --only knapsack PRISMIO_NOWRAP=0 python3 benchmarks/run.py --suite freestanding --only quicksort --only knapsack ``` ```text workload Prismio C Rust vs C vs Rust quicksort 9.99 M 10.10 M 12.96 M 0.99× 0.77× knapsack 3.14 M 5.12 M 7.40 M 0.61× 0.42× ``` With the access-fact marks off, quicksort reads 11.30 M again and knapsack stays at 3.14 M, because knapsack's gain is the guard's and not a mark's. The experiments used the compiler's own IR: save the module the pipeline is about to run on, internalise every function but `main`, run the optimiser and code generator for the freestanding target, and boot the result under QEMU with instruction counting. That reproduces the compiler's counts to within one tick, so an edit to the IR is a compiler change measured for free. Four rules came out of it. - **Attribute before you theorise.** Mark one operation at a time. Knapsack's regression with the counter alone was invisible to reasoning and obvious to a count. - **Version in loop form, not function form.** A function-level emulation of heapsort's guard measured +0.7%, while the same guard as the frontend's two loop copies measured −3.2%. The difference is pass ordering. - **Give C the same fact before crediting undefined overflow.** C's knapsack sped up by 39% from a one-line unreachable statement, with wrapping defined as well. - **Separate what a proof buys from what versioning costs.** Switching the marks off while keeping everything else does that in one binary. ## Prior art HotSpot's C2 compiler has the same contract, with 32-bit wrapping indices and 64-bit addresses. It narrows the sign extension of an index to the range its range check established, and gives a counted loop whose limit could overflow a check on entry, which corresponds to the range guard here. Go's prove pass builds induction-variable facts with explicit overflow checks, and a wrong overflow helper in that reasoning was the subject of CVE-2026-27143, which is the risk profile of fact tracking; an adversarial fixture covers shadowing, conditionally evaluated operands, loop heads and reassignment. Flang had the same problem with Fortran's 32-bit subscripts and now marks subscript arithmetic as non-wrapping by default. Popescu and Lopes (PLDI 2025) found the end-to-end gain from exploiting undefined behaviour in LLVM small, and that small optimiser improvements usually recovered the loss where removing it regressed. The numbered references are in the [developer reference](https://developers.prismio.org/performance/wrapping-integers-and-tight-loops). ## Reproduce it - [Range guard for sized arrays: src/ir/ranges.psm](https://github.com/prismio-lang/prismio/blob/main/src/ir/ranges.psm) - [Facts an unchecked access implies: src/ir/nowrap.psm](https://github.com/prismio-lang/prismio/blob/main/src/ir/nowrap.psm) - [Counted loops: src/ir/counters.psm](https://github.com/prismio-lang/prismio/blob/main/src/ir/counters.psm) - [What a local array holds: src/ir/contents.psm](https://github.com/prismio-lang/prismio/blob/main/src/ir/contents.psm) - [Benchmark runner](https://github.com/prismio-lang/prismio/blob/main/benchmarks/run.py) - [Full references and prior art in the developer reference](https://developers.prismio.org/performance/wrapping-integers-and-tight-loops) ## Questions this answers **Why not make signed overflow undefined, as C does?** It would change the language, and it did not help where it was measured. Over 62 hosted workloads the geometric mean against C++ moved from 0.8476 to 0.8465, a ratio of 0.9986, inside run-to-run noise. The four tight loops it did help are recovered by compiler analyses without changing what a program means. **Does C get these gains from undefined overflow?** Not for knapsack. Giving C the same fact in its source, with one unreachable-statement line, brought it to 3,139,120 instructions, and 3,139,104 with wrapping defined. Undefined overflow never supplied that fact. --- # A 2x slowdown with identical machine code - URL: https://research.prismio.org/notes/the-sdk-stamp-that-halved-malloc - Authors: Saksham Jaiswal - Published: 2026-10-01; updated 2026-10-22 - Measured on: Prismio 0.1.0 - Status: published - Keywords: benchmarks, macos, malloc, linker, methodology - License: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0) ## Abstract Three Prismio benchmarks ran 1.5 to 2.1 times slower when built by a compiler from a source checkout than by a packaged compiler, although the saved IR for their hot functions and the disassembly were identical. The difference was a single field in the Mach-O executable, the SDK version recorded in its build-version load command. macOS selects behaviour from that field, and a binary claiming an old SDK gets the older, slower malloc. Patching only that field restored full speed, which proved the cause. The link now passes the real SDK path, and the check is a one-line command worth running whenever a macOS timing moves without the IR moving. ## Key findings 1. tree_traversal read 1.8x, manual_alloc_churn 2.1x and s_expression_parse 1.5x slower with identical machine code. 2. The only difference was the SDK version recorded in the executable: sdk 11.0 against the installed 27.0. 3. Changing only that field in the slow binary restored full speed. A large, repeatable difference with identical machine code is not a timing mode and not a code change. The usual instruments say nothing is wrong: the saved IR is the same, and so is the disassembly of the hot functions. ## What differed Every Mach-O executable records the SDK it was linked against, in `LC_BUILD_VERSION`; it is the `sdk` line of `otool -l`. macOS keys behaviour on it, and a binary that claims an old SDK gets the older, slower `malloc`. A compiler built from a checkout linked with the pinned LLVM `clang`, which was given no sysroot, found no SDK to read, and stamped the minimum deployment version instead (`sdk 11.0`). Apple's `cc` stamps the real one (`sdk 27.0` on the measuring host). The three workloads that moved were `tree_traversal`, `manual_alloc_churn` and `s_expression_parse`. ## How it was proved Patching only that field in the slow binary restored full speed: ```bash vtool -set-build-version macos 11.0 27.0 -replace -output patched slow-binary ``` The fix passes `-isysroot` from `xcrun --show-sdk-path` at link time, with `SDKROOT` taking precedence, for host builds only. ## Checking for it Before believing a macOS timing that moved without the IR moving, read the field: ```bash otool -l path/to/binary | grep -A4 LC_BUILD_VERSION ``` The `sdk` value must be the installed SDK's version. ## Why an earlier check missed it An earlier A/B had shown only noise because it compared two compilers built from the same checkout, so the shared defect cancelled. When a result looks too good or too bad, compare against a packaged compiler. The wider rule for deciding whether a difference is real is in [Telling a real benchmark difference from noise](/papers/telling-a-difference-from-noise). ## Reproduce it - [runtime/build_driver.c: link_driver_command](https://github.com/prismio-lang/prismio/blob/main/runtime/build_driver.c) - [tools/package.py, the packaged compiler to compare against](https://github.com/prismio-lang/prismio/blob/main/tools/package.py) --- # Telling a real benchmark difference from noise - URL: https://research.prismio.org/papers/telling-a-difference-from-noise - Authors: Saksham Jaiswal - Published: 2026-10-01; updated 2026-10-22 - Measured on: Prismio 0.1.0 - Status: published - Keywords: benchmarks, statistics, noise, methodology, performance - License: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0) ## Abstract A fixed percentage ("within 4% is a tie") cannot decide whether one binary is faster than another on a workload that runs for under a millisecond. Process start, core placement and clock ramp-up produce two timing modes about a quarter apart, and the share of runs in each mode differs between binaries, so the same code can read as 19% faster or slower. We describe the rule the Prismio benchmark harness uses instead. A comparison is a win or a loss only when the ratio of medians and the ratio of best runs both leave a tolerance, and the tolerance for the medians grows with each workload's measured spread, using the interquartile range rather than the median absolute deviation. On a pair of real rows the rule calls a 1.22 times slowdown parity, because both languages show the same two clusters, and calls a smooth 1.18 times slowdown a loss. ## Key findings 1. Short workloads show two timing modes about a quarter apart, and the fraction of runs in each mode differs between two binaries of identical code. 2. A flat 4% threshold reported one workload as 19% faster when the best runs of the two languages were within 4% of each other. 3. Requiring both the ratio of medians and the ratio of best runs to leave their tolerances separates a smooth, real difference from a difference in which mode the runs landed in. 4. The interquartile range sees both timing modes; the median absolute deviation reported a bimodal workload's noise as nearly 0%. 5. The rule is not a significance test: five interleaved samples a side cannot support a p-value. A benchmark table has one question per row: is Prismio faster, slower, or level? Answering it with a fixed percentage fails in both directions on short workloads. A workload that runs for 0.6 ms is dominated by things no code change moves: process start, which core the scheduler picks, the clock ramping up. ## The problem: two timing modes Those effects show up as **two timing modes**, a fast one and one roughly a quarter slower, and the share of runs landing in each differs from one binary to the next. Consider five recorded runs of two workloads, in microseconds: | workload | Prismio | C++ | |---|---|---| | `edit_distance` | 566 577 588 **702 736** | 587 **709 726 734 743** | | `large_buffer_copy` | 6686 6704 6760 6793 7055 | 5678 5700 5735 5747 5769 | `large_buffer_copy` is a smooth cloud. Every Prismio run is slower than every C++ run, so the difference is real. `edit_distance` is two clusters, near 570 µs and near 720 µs, in **both** languages. The medians differ because three of Prismio's five runs fell in the fast cluster and four of C++'s five in the slow one, while the fastest runs, 566 and 587, are within 4% of each other. The code takes the same time in each mode. Under a flat 4% rule the median ratio, 0.81, would be reported as Prismio being 19% faster. That is as wrong as the opposite reading of a different set of five runs. ## The rule A comparison is a `win` or a `loss` only when **both** of these leave a tolerance, and is `parity` otherwise: 1. the ratio of **medians**, against the tolerance `max(parity, spread, floor / median)`; 2. the ratio of **best runs** (minimums), against `max(parity, floor / minimum)`. | term | value | meaning | |---|---|---| | `parity` | 0.04 | the flat floor the suite's alternating identical binaries measured as their own run-to-run difference | | `spread` | per workload | the larger of the two arms' interquartile range divided by their median | | `floor` | 25 000 ns | what process start, scheduling and clock ramp cost a run however short, as a fraction of the run | There are two ratios because noise only adds time. The best run is the steadiest estimate of what the code costs; the median says what a run usually costs. A real difference moves both. Equal best runs with different medians is the signature of timing modes, and is parity. The spread is the interquartile range, not the median absolute deviation. When more than half the runs sit in one mode the deviation is close to zero, so the older estimate reported `edit_distance`'s noise as nearly 0% and a 700 µs against 560 µs split as a loss. The interquartile range sees both modes. ## Result Applied to the rows above, and to a third much shorter workload: | workload | median ratio | best-run ratio | tolerance | verdict | |---|---|---|---|---| | `edit_distance` | 0.81 | 0.96 | ±21% (its own spread) | parity | | `large_buffer_copy` | 1.18 | 1.18 | ±4% | **loss** | | `blake3_chunk` (0.15 ms) | 1.04 | 1.06 | ±16% (the 25 µs floor) | parity | In a live run the same distinction appears in the summary. `edit_distance` at 1.22 times C++ is printed as parity, and `large_buffer_copy` at 1.16 times is printed as a loss, because the colour follows the verdict and not the ratio. ## Reproducing it ```bash python3 benchmarks/run.py --only edit_distance --only large_buffer_copy --runs 7 ``` Since results schema version 3 the verdict is written into the result file, so no reader has to recompute it: ```json "verdict": { "cpp": { "outcome": "parity", "ratio": 0.809, "best_ratio": 0.9631, "tolerance": 0.2126, "noise": 0.2126 } } ``` `outcome` is `win`, `parity` or `loss`. `ratio` and `best_ratio` are Prismio's time over the other arm's, so below 1 means Prismio is ahead. `tolerance` is what the medians were judged against, and `noise` is the spread it contains. ## Reading a disagreement When a row is called a loss and the verdict is in doubt, check the three numbers before the code. - **Samples in two clusters with equal minimums** are timing modes, and parity is the right answer. Rerun with more runs if a number is needed. - **A smooth cloud whose ratios agree, with a tolerance near 0.04,** is a real difference. Time the phases of the workload separately in both languages before changing the compiler. - **A large tolerance because the spread is large** means the verdict cannot be trusted either way. Increase the number of runs, close other work, and check the power state recorded with the results. ## Limits - The rule is **not a significance test**. Five samples a side cannot support a p-value, and the samples are interleaved rather than randomised, so they are not independent draws. - It **assumes noise only adds time**. A workload whose fastest run is an outlier in the other direction would be judged on a minimum that is not representative, so the raw samples should be read. - It does **not call timing modes a defect in either language**. If one binary lands in the slow mode more often for a systematic reason, that is a finding for a separate investigation. - The **25 µs floor was chosen from one host**. On a machine with faster process start it is generous. - An arm compiled with less information is slower for reasons unrelated to the language. The Prismio arm is built as one whole program; the C++ and Rust arms are built with link-time optimisation so that a constant passed across a file boundary reaches the code that uses it in all three. Without that, one graph workload divided by a run-time value in C++ and Rust and by a folded constant in Prismio, and read as an 8% loss that C++ at the same constant did not show. ## Reproduce it - [benchmarks/run.py: spread(), verdict(), verdicts()](https://github.com/prismio-lang/prismio/blob/main/benchmarks/run.py) - [Benchmark suite and reproduction steps](https://github.com/prismio-lang/prismio/tree/main/benchmarks) - [The same rule in the developer reference](https://developers.prismio.org/performance/telling-a-difference-from-noise) ## Questions this answers **Why not just treat anything within a few percent as a tie?** On a workload that runs for 0.6 ms, noise is not a few percent. Two timing modes about 25% apart appear in both languages, so a flat threshold turns which mode a process happened to land in into a verdict that no code change can fix. **Is this a statistical significance test?** No. The samples are interleaved rather than randomised, and five a side cannot support a p-value. The rule decides how much of a difference to believe, not how likely it is by chance. --- # Why Prismio's Int is 32 bits and wraps - URL: https://research.prismio.org/notes/int-is-32-bit-and-wraps - Authors: Saksham Jaiswal - Published: 2026-08-26; updated 2026-10-22 - Measured on: Prismio 0.1.0 - Status: published - Keywords: integers, overflow, performance, language-design - License: Apache-2.0 (https://www.apache.org/licenses/LICENSE-2.0) ## Abstract A default integer can be 64 bits so that it can index any array, or 32 bits for density. For Prismio we measured three things before choosing. A 32-bit index loop compiled to the same code and the same time as a 64-bit one in a counted loop. Making signed overflow undefined slowed three real programs slightly and moved the geometric mean over 62 workloads by 0.9986 times, inside noise. A 64-bit Int cost 1.33 times on a struct-field step over 20,000 records. The cost that remains is silent wraparound, which a checked mode can price at about four times and so only as a debug mode. ## Key findings 1. A counted loop with an i32 index, an i32 index with nsw, and an i64 index ran in 0.226, 0.225 and 0.225 ms. 2. Marking every signed add, subtract and multiply nsw made three real programs slower by 1.014x, 1.006x and 1.005x. 3. A 64-bit Int cost 1.33x on a struct-field step: 27.15 ms against 36.11 ms. The case for a 64-bit default integer is indexing, as in Go and Swift: an integer that can address any array. The case against is density, as in Rust's `i32` fallback. Three measurements settled it for Prismio, on an Apple M-series machine, decided on 2026-08-26. ## What was measured | question | result | |---|---| | Does a 32-bit index cost a sign-extend per access? | No. The same loop with an `i32` index, an `i32` index with `nsw`, and an `i64` index ran 0.226, 0.225 and 0.225 ms (stride) and 0.529, 0.526 and 0.526 ms (saxpy). AArch64 emits no `sxtw` in the loop and x86-64 one `movslq`, in setup. | | Would making overflow undefined help? | Not in time. Adding `nsw` to every `add`, `sub` and `mul i32` in three real programs made all three slower: 1.014×, 1.006×, 1.005×. Over the 62 hosted workloads it moved the geometric mean by 0.9986×, inside noise. | | What does a wider `Int` cost? | 1.33× on a struct-field step over 20,000 eight-field records (27.15 ms with `Int`, 36.11 ms with `I64`), and 1.76× to 2.15× in a C control. Twice the bytes per cache line is half the SIMD lanes. | The first row holds for a counted loop. A loop that stops on the data, such as the scans in a quicksort partition, pays one extension per access until something proves its counter cannot wrap. That is the subject of [Closing the gap to C on wrapping integer loops](/papers/wrapping-integers-and-tight-loops). ## The decision and its cost `Int` stays 32-bit and wraps. `I8`, `I16`, `I64` and the unsigned widths exist for data that needs them. The cost is real and was seen three times during the work: silent wraparound. A benchmark's own `Int` arm printed 1,856,082,944 where its `I64` arm printed 800,720,000,000. A checked mode is priced at 4.1× (stride) to 4.4× (saxpy), using clang's signed-overflow sanitizer as a stand-in, so overflow checking can only be a debug mode, which is what the `--overflow-checks` flag provides. ```bash prismio build --overflow-checks program.psm ```