Comment by YuechenLi

1 day ago

>Combined with the Rust rewrite, ICU changes, and identical code folding, Bun's binary size shrinks by ~20% on Linux & Windows.

People who are surprised by this probably has not seen what Zig code actually looks like. Zig's explicitness and lack of abstraction have a real cost that it is basically one of the most verbose programming languages I've ever seen, it's somehow even more verbose than Go. Basic features of modern languages like pattern matching and generics, and as you can see, having to manually clean up everything means that if you forget once, it's a memory leak. Having SOME abstraction is actually good if it prevents you from making mistakes.

Ironically, Zig is a programming language that's probably best written by LLMs, since they can actually tolerate the verbosity.

Not a compiler expert - shouldn't language verbosity and binary size be, at best, very loosely related?

  • I don't think you can draw the conclusion that source length and binary size are correlated. For example, in Rust:

        #[derive(Copy, Clone)]
        enum Expr {
            Int(i32),
            Add(i32, i32),
            Neg(i32),
        }
        
        fn eval(expr: Expr) -> i32 {
            match expr {
                Expr::Int(x) => x,
                Expr::Add(a, b) => a + b,
                Expr::Neg(x) => -x,
            }
        }
    

    Rust's enums can carry data. You can write the same thing in C, but because it does not have the enum feature, you have to do it yourself. They're sometimes called "tagged unions" for a reason, you use a union + a tag when doing it by hand:

        #include <stdint.h>
        
        typedef enum {
            EXPR_INT,
            EXPR_ADD,
            EXPR_NEG,
        } ExprTag;
        
        typedef struct {
            ExprTag tag;
            union {
                struct {
                    int32_t value;
                } Int;
        
                struct {
                    int32_t left;
                    int32_t right;
                } Add;
        
                struct {
                    int32_t value;
                } Neg;
            };
        } Expr;
        
        int32_t eval(Expr expr) {
            switch (expr.tag) {
                case EXPR_INT:
                    return expr.Int.value;
        
                case EXPR_ADD:
                    return expr.Add.left + expr.Add.right;
        
                case EXPR_NEG:
                    return -expr.Neg.value;
            }
        
            __builtin_unreachable();
        }
    

    I haven't actually compiled this, but it should compile to almost the exact same, if not literally the exact same, machine code. Yet one is way more verbose than the other.

    • I'm not sure individual examples is the right way to go about this. A correlation isn't a guarantee for every instance and it's easy to concoct individual examples which tell any story you'd like them to.

      To properly answer this you'd need to compare a large number of identical implementations written idiomatically in several languages and see if there is a correlation.

      If I were to throw my 2 cents in I'd say "a very weak correlation" is probably right. Not because verbose languages HAVE to result in more bloated code but because it seems to me languages fine having a lot of bloat in the syntax also tend to be languages fine having a lot of bloat in the implementation or attracted to abstraction (which never does seem to actually compile away fully in large projects, even though it often largely does).

      3 replies →

    • I think you are saying the same thing as benced - just because Zig source code is verbose is no reason to assume the binary should be larger.

      2 replies →

  • Fair point, I phrased that too broadly, and you are right about the loose correlation.

    What I was gesturing at, badly, was more that Zig’s low-abstraction / explicit-by-default syntax tends to have you write more boilerplate-y code in general that are more annoying to write and maintain, while not buying you enough over a language with better tooling and ecosystem and compiler optimization like Rust.

  • Why? Python is terse but has large binaries because of the runtime overhead. C++ is fairly verbose but can make useful binaries in double digit kib.

> Ironically, Zig is a programming language that's probably best written by LLMs, since they can tolerate actually tolerate the verbosity.

Rust in my opinion feels the same.

  • I have found LLMs struggle with Rust's constraints - they are optimized to produce code that passes the tests, not necessarily good code. So instead of working out lifetimes and borrowing, it will be happy to copy a buffer many times without thought. This means I have to still go through line by line to review and often rewrite either by hand or with another LLM iteration.

    There may be some prompting that can help with this but I suspect there is a fundamental tension between writing working code vs good code in LLMs. Go is popular for being simple, making it easy to jump in and write something fast and stable - minimizing the gap between working and good code probably helps out the LLMs a lot.

    • > There may be some prompting that can help

      Sounds like a good time to tell Claude to create a system wide memory of how to implement lifetimes and borrowing and what to NEVER do.

      Claude will then make the memory file. Usually memories help a lot. I see it think about it when it reasons through code.

  • Agree. But just because it feels the same doesn’t mean it compiles the same.

  • I don’t feel the verbosity with Rust. Haven’t written it in a while but now in the LLM era I’m looking forward to saying “sort out the lifetime errors for me”.

Zig is indeed verbose in some aspects, but not overall. For example, its `try error-union` syntax eliminates a lot of boilerplate code.

The main reason why Zig is verbose in some aspects is the main goal of Zig is program performance. It is a worthy tradeoff.

  • That goal doesn’t imply verbosity is necessary- just a style thing which is contrary to its goal of being a better C, given verbosity is the opposite of most C.

Abstraction doesn't necessarily lead to a smaller binary. Much of the bloat in modern software is indeed due to (bad) abstractions.

Naive was 4-9% on the initial pass.

Also note that the larger percentages were against already smaller binaries. That smells like there was a single large constant number that got saved somewhere rather than general improvements.

> After that initial shrinkage, the team explored more opportunities for binary size reduction using linker optimizations like Identical Code Folding, removing unused data from ICU, and lazily decompressing small parts of libicu with a zstd dictionary on-demand.

I'd be VERY interested in seeing what the individual effects of those parts were.

The twenty percent quoted is referring to the size of the compiled artifact (one assumes ELF or Mach-O).

Whether or not a language is verbose or obscure is very much about your coordinate system. Not unlike safety.

I think C is a reasonable zero for both things.

Zig is more succinct and safer than C while still being comparably ergonomic. Rust is (mostly) safer and more succinct than Zig while being dramatically less ergonomic (take it up with Wadler memory chads, no one likes affine types).

I like lean4, which is dramatically safer, more succinct, and more ergonomic than Rust.

But I can see why some would say it's a bit too succinct.

  • > Rust is (mostly) safer and more succinct than Zig while being dramatically less ergonomic

    This is just your opinion.

    • Well, it's my opinion. But it's also the opinion of the broader functional programming community from 1993 to the present day. This notably includes the quite serious Haskellers who designed Rust for the highly specific and demanding requirements of the Servo rendering engine in ~2010. Being as my two parents in web browser layout optimizations were both filed in 2009 I took considerable interest.

      It wasn't until 2014 that Orchard formalized the coeffect discharge calculus via indexed monad that makes a binary ownership semantic irretrievably sunsetted as a degenerate case.

      It's my opinion. I'm not concerned about how informed that opinion is.

      1 reply →