4 ms·
As usual, the programmer should look at the generated assembly, when reasoning about performance. Sometimes compilers are smart, optimizing away things people w
by yakubin 6y ago
As usual, the programmer should look at the generated assembly, when reasoning about performance. Sometimes compilers are smart, optimizing away things people wouldn't imagine they can, and sometimes they are really dumb, pessimizing code in unimaginable ways (to the extent that just deleting code from the output, which doesn't naturally correspond to anything in the source code, makes the output better). Relying on the compiler to perform magic without verifying the results is an antirational stance. Personally, I'd prefer if optimizations were applied based on what's visible in the source code, instead of the wild-west that we have now. E.g. inline function calls based on annotations left by the programmer, not because you (the compiler) feel like it.
The Rust compiler can be very good at optimizing iterator pipelines, however a while ago I wrote a function which didn't have any branches whatsoever in it, but the generated assembly was ridden with them. What could have been 5 instructions consisting of simple bit operations (everything done in registers, not loading anything from memory), ended up as 12 (? IIRC) ridden with conditional jumps. No matter how I changed the source code, which operations I used, the output was the same. Getting to the desired result shouldn't be stifled by the compiler having a mind of its own, making its own decisions. A sane default could be set, but some annotations to have a greater control over the result would be appreciated.
Optimizing compilers also make debugging harder. Making the the output and the source code closer together would help making sense of a debugging session.
- _ph_ 6y agoThis is one things I like so much about SBCL, it not only compiles the Lisp code directly to native code, with the disassemble function you can print out the generated code for each function to see how much the compiler was able to optimize a function. This is especially important in the context of dynamic languages to see where generic operators have to be called and where the type inferencer was able to compile it into an assembly instruction like integer multiplication.
- lispm 6y agoMany Lisp compilers do that. What makes SBCL special is a) using type information as compile-time assertions and b) the level of diagnostic output during compilation. The compiler explains in detail when operations can't be optimized further, for example because of lack of type information.
- _ph_ 6y agoWell, yes. How SBCL uses the type annotations as well as type inferencing to completely eliminate type checking throughout sections of code and generating highly efficient assembly code makes looking at the assembly output of SBCL especially interesting to look at. It gives you good feedback whether your type annotations had the desired optimization outcomes. I didn't want to indicate that other Lisp implementations wouldn't offer native compilation or a disassemble function.
- lispm 6y agoMany implementations also give us type inferencing and optimizations, but usually not compile time type checking like in SBCL. I would also kind of think that the level of type inferencing in SBCL is greater.
- pjc50 6y agoThis sounds tempting, but it would trigger a wave of unreadable, irrelevant micro optimization by many programmers. If you have a desired assembly result, write it yourself in the first place. The one concession to this I would like is a "constant time" marker for cryptography functions where it is an absolutely critical property that there be no data dependant branches. (This is a little different on GPUs where branches are so unreasonably expensive, and it sounds like that would also benefit from "no branches here" markers. The downside of this is that it makes valid code un compilable if the compiler can't find a branchless way of doing it.)
- wizzwizz4 6y agoI've been designing a programming language where nothing is a side-effect – including calling conventions, processor caches, branch predictors, locks, graphics modes, etc.. “Runtime has to be independent of the value of the input” would be how that language expresses "constant time" – and it would take into account every part of the compiler's model of the target architecture (which, by the way, is also described in the code) so for a sufficiently-simple, sufficiently-well-modelled architecture, the generated code could be provably Spectre-proof – given sufficient assumptions (Eis's take on Rust's `unsafe`). It's very early days, but I hope to get something usable by 2025.
- pjc50 6y ago> processor caches, branch predictors You can't "take this into account" because it requires state that isn't visible to you and also requires you to know the design of future hardware!
- wizzwizz4 6y agoIt requires state that can be inferred and manipulated. For the cache, assuming you know how large the cache is, you can just fill up the cache with useless pages, evicting whatever might be vulnerable to a timing attack, before having it loaded back in. Very slow, but it works. (You might be able to get away with less, depending on the OS's guarantees and what you can assume about how your program will run – and the language requires you to make those assumptions explicit, even if it's a case of "assume this manually-written assembly does the right thing without violating any invariants".) Yes, these tricks don't have forwards-compatible guarantees in the general case – but if the processor manufacturer makes guarantees about all members of a family, then you can optimise to those, and in any case having something that provably works in all supported processors (and can be ported to later processors if necessary, just by using an extra library (if the library is correct)) is still useful.