3 ms·
> The optimizer’s behavior is in some contexts load-bearing and has little margin for error (e.g. missing an optimization). Well, sure, sometimes, to an extent
by skybrian 2y ago
> The optimizer’s behavior is in some contexts load-bearing and has little margin for error (e.g. missing an optimization).
Well, sure, sometimes, to an extent. But if it's load-bearing, maybe that's a bug? You might have written non-portable code that won't last, because it depends on an implementation detail that isn't standardized.
There are widespread applications where performance isn't critical. For example, any time you do a network request. Web pages shouldn't break because the network is slow today, if you can possibly avoid it.
The web provides no performance guarantees, but it tries pretty hard to provide compatibility guarantees. Your code should, usually, work on new browser versions and on devices that haven't been released yet. New browsers will have different JIT compilers with different performance cliffs. And yet, there are websites written many years ago that still work.
When standardizing things, we need to be precise about what's standardized and what isn't, or protocols "rust shut" and can't be changed without breaking lots of stuff. Not standardizing performance is often a win. (With some exceptions like video game consoles where the hardware is standardized.)
Hyrum's law suggests that all compiler optimizations will eventually be load-bearing for someone, but we should usually try to avoid it. To make sure that the code is robust, perhaps performance should vary. Maybe it would be useful to have something like a chaos monkey for programs, where optimizations vary based on a random seed?
- bsder 2y ago> But if it's load-bearing, maybe that's a bug? If your "debug optimization" code is so slow as to be unusable (see: Rust), then your optimizations qualify as load-bearing. The problem is that "optimization level" needs a mindset change. The optimization levels should be "release", "small" and "experimental". "Release level" needs to be perfectly usable for debugging as well as in production--"debug level" should be "release level". Compilation time should be reasonable and run time should be functional. After that, for embedded, you should have "small level"--checks should get turned off and any optimizations that make code significantly bigger should get turned off (loop unrolling, for example). You might enable some optimizations that make compile time brutally slow. Finally, there should be an "experimental level" which tests out optimizations before they go into release. And there should be no "fast" optimization level. If your optimization is that situation specific, it should stay stuck in "experimental". And through all of this, the compiler should also eject files that carry enough information to allow debuggers to make sense of the compiled code, unwind the optimizations when the users asks, and present a coherent version of what is going on. This is actually where compilers really break down nowadays. The compiler needs to eject enough information and context that a debugger can unwind what is going on rather than being an afterthought. We need the equivalent of an LSP (Language Server Protocol) for debuggers.
- skybrian 2y agoFor the web, there are debuggers, but no apparent "debug mode."
- astrange 2y ago> And through all of this, the compiler should also eject files that carry enough information to allow debuggers to make sense of the compiled code, unwind the optimizations when the users asks, and present a coherent version of what is going on. You can't do this because some optimizations can't be seen through; most obvious one is identical code merging. If you're in a merged function then you don't know which of the original source lines you're looking at. I'd like to see a system which only had optimized compilation and used an interpreter for debugging.
- int_19h 2y agoYou can do that by having several different trampolines for the merged code that stash away a unique cookie on the stack to distinguish which function the code is currently executing. This is not zero-overhead, of course, but one can argue that the slight overhead is worth it given better error reporting and increased debuggability.
- bsder 2y ago> If you're in a merged function then you don't know which of the original source lines you're looking at. Then don't do that optimization. How much is gained versus the grief that is caused? Even when I did a lot of embedded device coding, I don't think that optimization ever saved me more than a couple dozen bytes of code. And, as you point out, the optimization is stupidly difficult for debuggers to to unwind. This is one of the disconnects of modern optimization. There is no good backpressure from downstream tools to say "Oy. That optimization is going to make our lives difficult. If you can't demonstrate a significant improvement, don't do it."
- astrange 2y agoIndeed, you can just not do it for important things - usually the error paths leading to a crash or exit. It's important for embedded software though. But that was a bad example; I should've said tail calls. Which are much more common and more important, because avoiding them blows out the stack for recursive code.