norir

Joined 4 years ago

Karma
1,117
Submissions
448
Member since
Nov 17
For those who haven't been trained by it, no discipline feels better at the time, but painful.
View on Hacker News

Recent submissions

Comment · 6 days ago
It is straightforward to pick an isa and then design an ir that closely mirrors it. Codegen from the ir the original isa is dead simple. Other isas are also straightforward except that it may be necessary to introduce virtual registers and implement certain instructions as multiple instructions if there isn't a 1:1 correspondence.

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.

Comment · 1 month ago
Anywhere that I could use reduce, I instead write a tail recursive function. This is also why I do not and will not ever choose python or javascript voluntarily.
Comment · 2 months ago
Comments like this just further my bias against analytic philosophy as a system that helps illuminate the path to human flourishing and truth.
Comment · 2 months ago
As someone who has written a jit compiler, I am puzzled by the claim that jitting requires write/execute permissions. When I have written a jit, I loaded some memory with read/write permissions using mmap. Once I filled in the generated code, I mprotected the region to read/execute before executing.

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.

Comment · 2 months ago
This reads as rather dismissive and not so humble. Compilers generating suboptimal and sometimes broken code very slowly is the norm, not the exception. Whether or not it's worth worrying about is indeed a development tradeoff but not one that should be so casually dismissed.
Comment · 2 months ago
It is simple to convert factorial to tail recursive form. In lua, which has tco:

    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.
Comment · 3 months ago
I would suggest adding objective metrics. How fast is this compiler? How long is the longest fuse program? How long does it take to compile? How fast at runtime is the fuse implementation of several benchmark programs compared to semantically equivalent programs written in other languages?

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?

Comment · 3 months ago
If you need better performance than a higher level language affords you, I would not recommend programming directly in asm. Instead, I would write a compiler. Raw asm is seductive since the start up cost is relatively low. You can get started in an afternoon. The trouble is that writing correct assembly is much harder than high level code. You have to hold in your head the register state at all times. You have to know if the function you are calling will clobber registers that you need after the call and manually save/restore them. This will slow down your velocity and the resulting code will be long and difficult to read. It will also rely on a lot of undocumented information that only resided in your head while writing and has since been evicted. Good luck debugging a program written in assembly that no one has looked at for two months.

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.

Comment · 3 months ago
I agree with your diagnosis but not entirely the solution. I think fixed sized integers in general are a non portable mistake in a language high level language. Instead, a language should provide arbitrary numeric range types and infer the result type of numeric operations. There should be automatic promotion to bignum when the number no longer fits in the machine word for the target platform. This keeps the focus on the data rather than the register sizes, which is incidental complexity. Where performance is critical, the programmer can annotate function types with specific fixed sized types that fit in registers and the compiler can statically enforce necessary bounds checks on possible overflows. I hypothesize that most software is so inefficient due to architectural flaws that it could be rewritten in a safe language with no undefined and/or unsafe numeric operations and still be at least as fast as the existing software even with the additional overhead of bignums and bounds checking.
Comment · 3 months ago
This piece would have been a lot more compelling if they had actually done science on selecting a language for compiler development. From what I can tell, they had an untested hypothesis that a low level systems language is necessary for a high performance compiler https://www.roc-lang.org/faq#self-hosted-compiler and from that concluded that their only choice besides rust was zig.

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.

Comment · 3 months ago
This infrastructure is also slow and leads to poor compilation times for any language that uses llvm as a backend. In an era of automatic code generation, this will become more and more of a problem as llvm compilation times will become a huge bottleneck. I am very bearish on llvm as a technology and while I will acknowledge its influence, I expect that it is at or near its peak and market share will decline dramatically over the next five to ten years.
Comment · 3 months ago
This feels like it should have been a warning rather than an optimization in the first place. In my opinion, dead code elimination should only be done during link time optimization where it can be proven that branches are not taken given the whole program information. If there is an unused assignment regardless of the branch, the compiler could emit a warning so the user can do their own dead code elimination, or choose to suppress/ignore the warning. The worst thing a compiler can do is silently incorrectly apply an optimization.

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.

Comment · 4 months ago
Perhaps then it would be better to not use tools of this level of complexity.
Comment · 4 months ago
I am actively opposed to this design for a first compiler. There is no need for a lexer with a recursive descent parser. Register allocation is also an unnecessary distraction. It is better for a first compiler to compile to a higher level language in which neither register assignment nor memory management are necessary.

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.

Comment · 4 months ago
Terry Tao is a next level vibe coder: he inspires people to do his vibe coding for him. As someone with a background in advanced math, though never even close to Tao's level, I find myself skeptical about this type of mathematics. I don't personally find it beautiful and it feels like the line between the profound and the trivial (as in of minimal importance not difficulty) is blurry. One could argue for pure mathematics that is of no practical utility but is aesthetically beautiful, but I struggle to see the beauty in a gargantuan lean proof constructed by 100 different people. Perhaps this work will lead to deeper insight about the universe and the human condition, but I catch a whiff of problem solving for the sake of problem solving untethered from a deeper sense of purpose and meaning.

Live from the official Hacker News API

A modern reader for Hacker News · SvelteKit + Skeleton