norir
Joined 4 years ago
Joined 4 years ago
I think the op is far too pessimistic about code size and performance. TCC for example produces terrible machine code but I rarely see 3x size or performance diffs compared to clang (at least for the kinds of programs I tend to write). It is also very easy to find edge cases where the clang optimizer makes heinously bad decisions. In one trivial example, I've written a program that has a helper function with no arguments that simply prints "Hello, world!" and a main function that calls the helper 1 million times. With optimizations, the version of clang on my computer inlined the helper call 1 million times, which made the binary I believe 4-6x bigger than -O0. With -O0, I believe compilation took 6 seconds compared to something like 15 minutes with -O2 (tcc took maybe 250ms and produced a binary that I think was about 2x the size of gcc -O0). All that is to say, I have less faith in the quality of optimizing compilers than most.
The drawback to this approach is there can be some bloat because you can only mprotect at page granulariy so a jitted function that only takes say 10 bytes to represent would take up a full page in memory, but this is extreme and in practice, the overhead is unlikely to be worth worrying about.
local factorial do
local function impl(n, acc)
if n == 1 then
return acc
else
return impl(n - 1, acc * n)
end
end
factorial = function(n)
if n < 0 then
error("factorial input is negative")
elseif n <= 1 then
return 1
else
return impl(n - 1, n)
end
end
end
You could replace impl with an imperative loop: local acc = 1
repeat
acc = acc * n
n = n - 1
until n == 1
return acc
Personally, I find this ugly compared to the tail recursive solution. The loop version only seems more natural if you primarily think in loops. Tail recursion is strictly more powerful than looping since every imperative loop can trivially be converted to a tail recursive function, but the reverse is not true.How expressive is fuse? How long are equivalent programs written in fuse/rust/scala/haskell?
Can you show me a bug that the fuse compiler catches but some or all of the competition doesn't?
On the other hand, if you write your own non optimizing compiler, you can avoid a lot of these problems by, for example, tracking what registers a function writes and ensuring they are saved before a call and restored after. Then you can actually get the raw performance of handwritten asm without the pitfalls (the resulting code would still he harder to read and maintain than equivalent high level code, but at least it would be tractable). Even better, you can write your own high level assembler that is actually portable to other architectures. For example, instead of directly modeling x86_64, your compiler can model a cpu with 16 general purpose registers and a set of instructions that map to x86_64 instructions. An arm port would be straightforward since arm also has 16 general purpose registers and you can model x86_64 instructions as one or more arm instructions (and you have extra registers for x86_64 instructions that must be modeled as multiple arm instructions). Or you could do the reverse and model 32 general purpose registers using arm instructions and use predefined memory slots as virtual registers on x86_64.
I know from experience that this initial assumption is wrong. Compiler performance is dominated by algorithms. The fastes managed languages tend to be at worst within a factor of two for wall time on any given algorithm. Algorithmic differences can be unbounded in their performance gaps. Zig itself is a perfect counterexample to the theory that writing a compiler in a low level systems language will lead to a fast compiler. Roc seems to compile at around 15k lines per second. That is not fast. There were evidently compilers written in ml that did 3k likes per second in 1998 https://flint.cs.yale.edu/cs421/case-for-ml.html
The zig rewrite of roc looks like the author's second compiler. Compiler and language design is a skill like any other and from my vantage point, they appear to have overcommitted to an initial design at the expense of developing their higher level design skills. In my opinion, the best thing they could do for the future of roc is stop working on their current compiler and use it to write a self hosting compiler for a much smaller subset of roc. They should be able to do that in less than 10k lines of code. They might even find that their self hosting compiler is faster than their zig based bootstrap compiler for the self hosted subset of roc. If the self hosting compiler is inadequate. Now they at least have identified a smaller useful subset of roc and can experiment with different compiler implementations in 10k likes of code rather than 300k lines of code. Then they could actually test the theory of whether or not a low level language is necessary to meet whatever arbitrary compiler performance goals they have.
By self hosting, they would also discover what roc features actually matter and they would spend much more time actually writing roc code. The features that are needed to write a self hosted compiler are all features that are generally useful. By improving the self hosted compiler, they also improve downstream programs.
Of course, I also feel this way about the vast majority of optimizations. If the compiler can optimize a piece of code, it can also show the user what it thinks the optimal code would be so that they can rewrite it themselves, if they so choose. This both prevents these kinds of miscompiles and prevents compilation times from exploding because the compiler doesn't need to do much work, it primarily just translates the human readable code into machine code.
Optimizing compilers are suboptimal in that they waste enormous amount of time optimizing code that can't or needn't be optimized and even where the optimizations are helpful, they are opaque and at risk of unexpectedly regressing both due to small changes at the source code level or changes in the compiler optimizer, both of which are quite insidious.
If instead of optimizing compilers, we had languages that allowed for seamless interop between low level and high level functions, then perhaps an llm becomes the optimizer (or you can invoke the compiler to optimize a specific function by source level rewrite). The benefit of this compared to a traditional optimizing compiler is that the optimization is done once per function and never repeated (until prompted) and the implementation is human readable and not buried in a binary. Moreover, and perhaps even more importantly, by not doing optimizations in the compiler, compilation times can be much faster, easily 100-1000x than state of the art optimizing compiler, while generating equivalent or even better runtime performance. As it has been said: premature optimization is the root of all evil.