12 comments

[ 3.8 ms ] story [ 32.8 ms ] thread
> our optimisation relies on having r < count, but if count is zero this condition cannot be met. [Since taking the remainder of a division by zero is undefined behaviour, the compiler would be well within its rights to assume that count != 0 and proceed with the transformation.]

Interestingly, even if you define division by zero to produce the remainder equal to the dividend, this optimization (replacing "(r + 1) % count" with "r + 1 == count ? 0 : r + 1") would still be legal.

Interesting. It all seems very brittle, though. And that something has gone very wrong with our ecosystem of tools, languages, and processes when it becomes advisable to massage source until specific passes in a specific version LLVM don't mess things up for other passes.

Not picking on the OA in the slightest; just thinking in terms of holistic system design. If you know what you want to happen, and you are smart enough to introspect the behavior of the tool and decide that it didnt happen, you are more than smart enough to just write it correctly in the first place.

Perhaps that is unrealistic, perhaps there is a hidden iceberg of necessary but convolutive optimizations no human could realistically or legibly write. But ok, where do you really need to engage in this kind of optimization golf? Inlined functions?

Ok, what about this targeted language feature for a future-day Zig:

1. Write an ordinary zig function 2. Write inline assembly version of that function 3. Write a "comptime assert" that first compiles to second, which only "runs" for the relevant arch. 4. What should that assert mean? That the compiler just uses your assembly version instead, but _also_ uses existing compiler machinery or an external theorem prover to verify they "behave the same up to X", for customizable values of X

That has the right feel, maybe. You are "pinning" specific, vetted, optimizations without compromising the intent, readability, or correctness of your code. And easy iteration is possible, because a failing comptime assert will just dump the assembly; you can even start with an empty manual impl.

I think a better approach might be automated performance regression tests. That's checking the property you probably actually care about directly (performance) and leaves the compiler (and other engineers) some leeway to do better without breaking the test.

Actually setting up a robust system for perf regression tests is tricky though...

It is brittle and a leaky abstraction. Code is already too complex, and correctness and time complexity are far more important than these micro-optimizations. Although I think your language feature seems reasonable.
(Author here)

>It all seems very brittle, though. And that something has gone very wrong with our ecosystem of tools, languages, and processes when it becomes advisable to massage source until specific passes in a specific version LLVM don't mess things up for other passes.

I would say the main takeaway is actually to not do that, precisely because it's brittle, difficult to understand, and can backfire by making things harder for the compiler. As I point out at the end, the vast majority of developers will be better served by sticking to common idioms and making their intent as clear as possible using language facilities, rather than trying to outsmart the compiler or, conversely, relying excessively on the optimiser as a magic black box.

That being said, I do find it helpful to understand the broad strokes of the optimisation pipeline (things like "callees are optimised before callers", "maintaining invariants enables more simplifications", or "early simplifications are better") to make the most of it. Like with any other tool, mastery means letting it do its job most of the time but knowing when and where to step in if necessary.

Do not check OP's art on a work machine lol
> Here, the simplification analysis uses the conditional hidden in the assert() macro to figure out that we only execute the urem instruction if cur < count (see the isImpliedByDomCondition() method). This can have the unexpected consequence of making builds with assertions enabled faster in some cases; we can restore performance by replacing disabled assertions with assume attributes, which have no runtime impact.

It surprises me that the compiler doesn't still take the inference from the assert and just disable emitting the code to perform the check. Over the last 15 years I've worked on many codebases that are written with unnecessary asserts, partly as documentation, but maybe because people assumed it helped the compiler.

I've also worked on many codebases (and written code like this on my own projects) where the code looks like: assert(condition); if (condition) { ... } because I still want that safety check in release builds, and want the exception in debug builds but absolutely not ever in release builds.