Four Months of Zig

(gracefulliberty.com)

82 points | by ibobev 7 hours ago

11 comments

  • i2talics 4 hours ago
    Why does everyone love being able to control the allocator in Zig? I rarely want this low level control over the allocator, I would rather have an easy to use interface to malloc/free (such as RAII) or would prefer the language push me to avoiding allocations when unnecessary. C and Zig users are always talking about how arena allocators are easier to use than the alternative but I disagree because the alternative is extremely easy to use when the language makes it so.

    Arenas have a lot of footguns like growing out of control if you don't have space in your logic to delete them (e.g. suppose every new HTTP connection required obtaining a new allocation from the primary arena -- now you have a memory leak). They also aren't necessarily faster than malloc: I remember hearing a case where mimalloc was faster than an arena for a one-shot program like a linker because reclaiming space improved cache locality. Plus it's difficult to implement things like growable vectors or hashmaps using an arena without wasting space.

    I just don't see how "futz about with the allocator constantly" and "use an arena" is actually a good default.

    • awesan 4 hours ago
      It is a good default for lib code because it makes that code truly re-usable. The same is true for great C libraries, they let you provide a block of memory instead of calling malloc inside. The library should not get to decide if/when allocations should be done.

      In Zig app code you don't have to pass allocators all over if you don't want to, that's your code so do what you want. But for libraries it's the right thing to do because it puts the caller in control.

      The reason you have to pass allocators instead of making this implicit like in e.g. Odin, is because Zig just takes a firm stance against implicit logic, firmer than any other language I've ever used. The end result is that people who like Zig tend to like passing allocators around even in their own code.

      • elcritch 2 hours ago
        > Zig just takes a firm stance against implicit logic

        I bought that as Zig's M.O., but then came the automatic "async/IO" colorless functions which is a huge chunk of implicit and hidden logic.

        That's more implicit logic than what's in say JavaScript async AFAICT. Arbitrary int sizes with implicit casting rules, etc. More recently was the attempts of memory safety via an some sort of (optional) implicit GC mode. Personally I don't get it.

        • Mond_ 55 minutes ago
          Are you sure you're up to date?

          The async design changed multiple times, and the current approach is that of requiring passing an I/O interface that encapsulates the logic, same approach as for passing around an allocator.

          This effectively makes all I/O bound code "quasi-async", and it's all explicit and the caller is in control.

      • latch 2 hours ago
        The library developer doesn't know what allocator will be passed, and the library user doesn't necessarily know how it will be used. They share an interface, so it will [probably] work, but it might not behave (mem/perf wise) as either expects. ArenaAllocators in particular don't play great with ArrayLists or nested ArenaAllocators.

        I don't think it's a huge problem, but it's more subtle than an explicit allocator is a better because it provides more flexibility.

    • the-tomster 3 hours ago
      If you don't like arenas then that will push you towards avoiding allocations :)

      I don't love malloc / free, managing frees one-to-one with allocations is a nightmare. An HTTP server is actually a great example of a good fit for arenas... you can have an arena created per connection, and then free the arena when the connection closes. Now all of your allocations during that connection are guaranteed to be freed instead of having to keep track of them individually. Need something to live across connections? Use the general purpose allocator for those particular things.

      For one shot programs, why bother freeing anything at all? The gpa or malloc or whatever else is fine there.

      Personally I mostly lean on fixed-size buffers, but if I'm writing Zig it's just little toy programs where blowing up when the buffer runs out of room is fine. But for big non-toy programs it might be good to use fixed-size buffers anyway and define your limits in terms of things actually relevant to your domain.

      RAII and smart pointers are fine too, but C++ has you covered if you want those. Zig is specifically avoiding that kind of invisible code that destructors bring you.

      • grg0 2 hours ago
        > Now all of your allocations during that connection are guaranteed to be freed instead of having to keep track of them individually

        And how do you size the arena?

        > keep track of them individually

        I think this is an exaggeration of the problem. This is what RAII semantics in C++ are for.

        > avoiding that kind of invisible code

        The invisible code is traded for defer statements right after initialization that are obvious and therefore mostly just noise, and also a problem if you forget to call them. This isn't an issue with RAII. I get the point of not wanting invisible code, but defers don't seem much of an improvement either. A trade-off at best.

        • the-tomster 1 hour ago
          > And how do you size the arena?

          Two options: you either say "eh 20MB per request ought to be big enough" and resize it later if you need to. Or you take time to figure out reasonable limits in terms of whatever domain you're dealing with, and return descriptive errors when those limits are exceeded. Maybe you're saying your service supports up to N HTTP headers and M query parameters in the URL and so on. For almost anything there's a limit you can place that's reasonable. Having no limits at all just means the limit is when you run out of memory / disk / whatever.

          If you like RAII and C++ just use that though, Zig is an alternative not an upgrade, imo.

      • i2talics 3 hours ago
        > If you don't like arenas then that will push you towards avoiding allocations

        I mean push in the sense of a pit-of-success, not making me do things because the alternative is unpalatable.

        > managing frees one-to-one with allocations is a nightmare

        It really isn't a nightmare if your language has tools to help you. I'm never particularly concerned about frees in Rust outside of thinking about room to optimize.

        > Need something to live across connections? Use the general purpose allocator for those particular things.

        Well, this is a determination you have to make very carefully and if you screw it up (like in the situation I was trying to outline) you have a subtle bug.

        > Personally I mostly lean on fixed-size buffers, but if I'm writing Zig it's just little toy programs where blowing up when the buffer runs out of room is fine.

        What's also fine is using malloc, which will have perfectly reasonable performance for your toy programs.

        This is reinforcing my belief that Zig programmers actually just have a sort of aesthetic attachment to the idea of managing their allocations themselves and this has very little to do with the actual practical concerns of writing software that works. I'm a lot more sympathetic to the other reply that talked about library code improving flexibility for those relying on the library, than this comment that only thinks about the case where everything goes perfectly and using a fixed size buffer for no reason.

        • nvme0n1p1 3 hours ago
          > I'm never particularly concerned about frees in Rust outside of thinking about room to optimize.

          Zig's position, love it or hate it, is that you should be thinking about it. Dropping a Vec<String> in Rust is O(n). Have you ever clicked the X on an app and it takes 10 seconds to disappear? Stuff like this is part of the problem. You shouldn't have to traverse your process's whole memory space just to call exit(0).

          If you have to write that for loop, the O(n) will jump out at you and push you towards an arena or FixedBufferAllocator or std.process.cleanExit, or any other solution to avoid the loop.

          • kibwen 1 hour ago
            Dropping vecs that contain strings does not make any Rust code in the real world slow. You would need to have a vec containing billions of strings, occupying several gigs of memory, before dropping such a structure took any perceptible amount of time. And for extreme cases like that, you can simply use arenas. The bumpalo crate is approximately as old as Zig is. The reason that people so rarely turn to arenas in Rust isn't because you can't do them, because you've always been able to, but rather because arenas are a very specific solution to a specific sort of memory access pattern, and going out of your way to strictly adhere to that pattern doesn't produce measurable differences for most software.
        • the-tomster 3 hours ago
          > This is reinforcing my belief that Zig programmers actually just have a sort of aesthetic attachment to the idea of managing their allocations themselves and this has very little to do with the actual practical concerns of writing software that works.

          Indeed if you don't want to manage everything yourself I don't think there's a need to use Zig. The whole pitch is making everything explicit and not hiding control flow. And I do think that limits its usefulness to smaller-scale codebases.

          Writing quality software doesn't require much from the programming language until your codebase gets very large and you're collaborating with a ton of people. If I'm expecting a codebase to grow to a large scale I'm not reaching for Zig for that. I don't trust _myself_ to keep that much Zig code tidy and correct let alone a big mob of developers and agents :)

    • amw-zero 1 hour ago
      If you had to pick a default, an arena allocator would definitely be it. Although even better would be a fixed-size arena that can never grow, like Tigerbeetle does.

      I don't think the footguns you're talking about are real by the way.

    • giancarlostoro 2 hours ago
      The biggest interesting thing to me about Zig is that is is a true spiritual successor to C.
    • elendilm 3 hours ago
      mimalloc aggressively reclaims space and it benefits the use case in our Dip database built on rust.

      This is for a long running process where consuming compute and memory should only occur on processing a request and must revert to a baseline memory consumption once the request is completed.

      malloc with default allocator always gradually increases the memory consumption. This continues with enough request bursts on our testing until the process runs out of memory. mimalloc doesn't.

      On mobile, we are stuck with the default allocator. On mobiles, the phenomenon of the default allocator consuming memory is reproducible even with normal Qt apps. It is what it is.

      • grg0 2 hours ago
        > This continues with enough request bursts on our testing until the process runs out of memory

        Curious, how much memory and how many requests does that take to exhaust?

  • delifue 50 minutes ago
    The article says some Zig advantages but it's worth noting that all these advantages are tradeoffs.

    Zig makes handling error easy but makes retaining error information hard. The result is that Zig developers either write more code than other languages to store diagnostic information, or just simply discard diagnostic information. The second makes it much harder to debug because the useful error information is discarded.

    About the TigerStyle, it's worth noting that TigerBeetle doesn't even support variable-sized string type. You can store fixed-sized data, such as a string that's at most 32 bytes. What if the order information contains string longer than 32 bytes? Then you need to store that string in antoher database. TigerBeetle is not a general database. Static memory allocation doesn't fit most applicaitons.

    The fallable allocation is useful in embedded system and OS development etc., but useless in common application programming, because OOM results in crash rather than failed allocaiton.

  • yje07039 7 minutes ago
    How much of the four months went to keeping up with breaking changes? Thats been my main blocker with zig, more than the language itself.
  • 22c 4 hours ago
    First I'm hearing of TigerStyle.

    > The time to solve performance, and get the 1000x wins, is in the design phase, when you can't profile.

    How does this make practical sense? I find it immensely easier to fix performance issues by profiling, benchmarking, finding hotspots. Things that weren't obviously performance bottlenecks during design become obvious when you profile.

    Interesting write-up though. I haven't properly tried zig but I like reading these "initial impressions" style articles about it.

    • chrisco255 3 hours ago
      If you understand how the CPU, memory, network, and disk are processing your program and its structures, you can design your programs to optimize for the hardware.

      Full guide for ref: https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TI...

      • austin-cheney 3 hours ago
        This is also true for garbage collected languages, JavaScript in my case, but it requires a custom profiler and a bit more digging while asking better questions.
        • nvme0n1p1 3 hours ago
          You'll have a more direct path towards better performance if the questions you're asking are closer to "how does a CPU work", and further away from "what hoops do I have to jump through in 2026 to avoid optimization bailouts after V8's latest round of changes"
      • elendilm 3 hours ago
        True. The guide is an interesting read. When you are deep in the trenches, most of what the github link says is just common sense.

        Eg: < Optimize for the slowest resources first (network, disk, memory, CPU) in that order, after compensating for the frequency of usage, because faster resources may be used many times more. For example, a memory cache miss may be as expensive as a disk fsync, if it happens many times more.>

        This is just obvious common sense. Great read.

      • Ygg2 3 hours ago
        Yeah, except the CPUs are complex, you might not understand ALL the details of a working program (like randomising memory layout).

        Plus not all speed-ups are made equal. Some speedups of a part of program can have no or even negative impact (as in making part of program faster slows down overall program execution).

        • grg0 2 hours ago
          The truth is really a middle ground and the fact that you won't get the design right the first time around until you have built that hardware programming experience and learned from past mistakes. But this statement doesn't make for a good marketing stunt.
    • vlovich123 1 hour ago
      While I don't agree TigerStyle inherently actually helps you with this, design and architectural mistakes don't usually get solved in the way you're describing. You sometimes have to rethink how to structure your entire codebase. Sometimes you can evolve an architectural improvement but sometimes it requires a complete rewrite and there's no way to get outside of that path dependency.
    • rcxdude 2 hours ago
      There's a different between fixing performance mistakes, which tend to be obvious in profiling as big hotspots, and designing for maximum performance, which requires that you start, really, with the specification of what your application is doing, then follow that through to architecture and design with a strong view to reducing unnecessary work and staying within the operations that can be done at high speed with current systems.
      • Ar-Curunir 1 hour ago
        And Zig doesn’t seem to help with most of that!

        If you’re already in your spec thinking about performance concerns at that level, then you can get pretty good performance in almost any language, though some languages might make it a little (lot) easier than others. But again I don’t think eg Zig vs Rust would be a big perf difference there.

  • michidk 4 hours ago
    Pretty nice! I wrote a similar piece two years ago: https://blog.lohr.dev/after-a-day-of-programming-in-zig Great to see things are moving in the right direction.
  • Atlas48 4 hours ago
    I can't wait until Zig is a little more stable, I'd love to get into it more because the metaprogramming seems like such a draw.
  • alexboehm 3 hours ago
    I agree with anyzig, it's amazing.

    I made a PR to add a default zig, which was rejected, I've been using it ever since though.

    https://github.com/marler8997/anyzig/pull/71

  • paaloeye 5 hours ago
    Does ReTangled have anything to do with https://tangled.org?
    • contact9879 4 hours ago
      it’s hyperlinked to the repo where you can see it has nothing to do with tangled
      • paaloeye 4 hours ago
        I know but the name is so sus
  • LoganDark 5 hours ago
    > In Rust you can use ?, which is quicker than unwrap, but you also have to do something like Result<Type, Box<dyn std::error::Error>

    No, you don't! When returning Result<T, E>, you can use ? on any Result<T2, E2> where E: From<E2>. In other words E doesn't have to be Box<dyn>, it just has to satisfy From<E2>.

    Crates like thiserror make it easy to define your own error types that can be easily converted from all the other error types you can encounter in your functions! I do this whenever it doesn't make sense to use an inner error type directly, or whenever a function does its own validation that deserves its own error variant.

    On top of that, I tend to also have error variants for each place in the function rather than only for each type of inner error. At error sites I do something like `.map_err(MyError::PlaceInTheFunction)?` rather than only `?`, and then errors become like a very efficient backtrace :)

    • Graziano_M 5 hours ago
      I think you're proving the author's point.
      • GreenDolphinSys 5 hours ago
        They're saying that you don't always have to return Result<T, Box<dyn Error>>, you can implement the From<Error> trait on the type and use ? as usual.

        In other words implementing that trait lets you set a return type like Result<T, CustomAppError> use ? on it.

        • LoganDark 5 hours ago
          I think they're saying the author's point is that it's more fiddly in Rust (even if there are strong, well-regarded fiddles to use)
          • Graziano_M 5 hours ago
            Yes, and your other part about the crate.

            > There's also Zig-style composable errors with yet another library.

            • LoganDark 5 hours ago
              Ah, it's behind a link!

              While I'd push back against implying From<E2> is part of the crate, thiserror is an idiomatic approach these days, so I can understand the confusion. I can totally see how they could come to favor Zig if this behavior is "the default", as opposed to Rust where you'd usually see it done with "yet another library".

              And so I see "the author's point" has another meaning now: that Zig-style errors are good :)

              Edit: oh, after looking into Zig-style errors more, this is actually not how they work! They don't nest, they just exist in a global namespace, so they don't come with any location data themselves, but you can compose sets of them. Less powerful, but certainly more appealing to some :) I think out-parameters can be used as a side channel for error context.

              • Graziano_M 4 hours ago
                Yeah I 1. wish they were properly composable and 2. think Rust would get less flak if these 'idiomatic' crates were just pulled into the standard library to avoid some of the dependency mayhem.
          • GreenDolphinSys 4 hours ago
            I don't think errors are fiddly in rust. They're easy to compose.
            • LoganDark 2 hours ago
              Hey, I find it super easy and that's why I commented! By fiddly I just mean there are more steps/choices.
      • tcfhgj 5 hours ago
        how so?
  • llm_nerd 3 hours ago
    Sort of related from yesterday - https://news.ycombinator.com/item?id=50247203

    (about zig's reimplementation of the libc, and some rough edges)

    • Numerlor 1 hour ago
      Zig's reimplementation of all of the standard libraries even though it's possible to use the existing ones is both baffling to me and kills usage of the implementation in my eyes.

      Redoing critical parts such as crypto and compression is crazy in this day and age, hell they even have issues and merge requests open for issues that'd be headliners if found in any major language

      • colejohnson66 1 hour ago
        Not just that, but on Windows, they're bypassing the actually guaranteed stable Win32 API in favor of linking directly to the lower-level components. Completely destroying any forward compatibility!

        https://codeberg.org/ziglang/zig/issues/31131

        It's the kind of change that makes me read Zig as not trying to be a serious language. Not that there's anything wrong with that, but the way Zig positions itself is at odds with the completely bizarre takes they do.

  • paaloeye 6 hours ago
    $curl: (6) Could not resolve host: gracefulliberty.com HN effect?