[HN Gopher] Low-Level Optimization with Zig
___________________________________________________________________
Low-Level Optimization with Zig
Author : Retro_Dev
Score : 227 points
Date : 2025-06-07 07:26 UTC (15 hours ago)
(HTM) web link (alloc.dev)
(TXT) w3m dump (alloc.dev)
| flohofwoe wrote:
| > I love Zig for it's verbosity.
|
| I love Zig too, but this just sounds wrong :)
|
| For instance, C is clearly too sloppy in many corners, but Zig
| might (currently) swing the pendulum a bit too far into the
| opposite direction and require too much 'annotation noise',
| especially when it comes to explicit integer casting in math
| expressions (I wrote about that a bit here:
| https://floooh.github.io/2024/08/24/zig-and-emulators.html).
|
| When it comes to performance: IME when Zig code is faster than
| similar C code then it is usually because of Zig's more
| aggressive LLVM optimization settings (e.g. Zig compiles with
| -march=native and does whole-program-optimization by default,
| since all Zig code in a project is compiled as a single
| compilation unit). Pretty much all 'tricks' like using
| unreachable as optimization hints are also possible in C,
| although sometimes only via non-standard language extensions.
|
| C compilers (especially Clang) are also very aggressive about
| constant folding, and can reduce large swaths of constant-
| foldable code even with deep callstacks, so that in the end there
| often isn't much of a difference to Zig's comptime when it comes
| to codegen (the good thing about comptime is of course that it
| will not silently fall back to runtime code - and non-comptime
| code is still of course subject to the same constant-folding
| optimizations as in C - e.g. if a "pure" non-comptime function is
| called with constant args, the compiler will still replace the
| function call with its result).
|
| TL;DR: if your C code runs slower than your Zig code, check your
| C compiler settings. After all, the optimization heavylifting all
| happens down in LLVM :)
| Retro_Dev wrote:
| Ahh perhaps I need to clarify:
|
| I don't love the noise of Zig, but I love the ability to
| clearly express my intent and the detail of my code in Zig. As
| for arithmetic, I agree that it is a bit _too_ verbose at the
| moment. Hopefully some variant of
| https://github.com/ziglang/zig/issues/3806 will fix this.
|
| I fully agree with your TL;DR there, but would emphasize that
| gaining the same optimizations is easier in Zig due to how
| builtins and unreachable are built into the language, rather
| than needing gcc and llvm intrinsics like
| __builtin_unreachable() -
| https://gcc.gnu.org/onlinedocs/gcc-4.5.0/gcc/Other-Builtins....
|
| It's my dream that LLVM will improve to the point that we don't
| need further annotation to enable positive optimization
| transformations. At that point though, is there really a
| purpose to using a low level language?
| flohofwoe wrote:
| Yeah indeed. Having access to all those 'low-level tweaks'
| without having to deal with non-standard language extensions
| which are different in each C compiler (if supported at all)
| is definitely a good reason to use Zig.
|
| One thing I was wondering, since most of Zig's builtins seem
| to map directly to LLVM features, if and how this will affect
| the future 'LLVM divorce'.
| Retro_Dev wrote:
| Good question! The TL;DR as I understand it is that it
| won't matter too much. For example, the self-hosted x86_64
| backend (which is coincidentally becoming default for
| debugging on linux right now -
| https://github.com/ziglang/zig/pull/24072) has full support
| for most (all?) builtins. I don't think that we need to
| worry about that.
|
| It's an interesting question about how Zig will handle
| additional builtins and data representations. The current
| way I understand it is that there's an additional opt-in
| translation layer that converts unsupported/complicated IR
| to IR which the backend can handle. This is referred to as
| the compiler's "Legalize" stage. It should help to reduce
| this issue, and perhaps even make backends like
| https://github.com/xoreaxeaxeax/movfuscator possible :)
| matu3ba wrote:
| > LLVM will improve to the point that we don't need further
| annotation to enable positive optimization transformations
|
| That is quite a long way to go, since the following formal
| specs/models are missing to make LLVM + user config possible:
|
| - hardware semantics, specifically around timing behavior and
| (if used) weak memory
|
| - memory synchronization semantics for weak memory systems
| with ideas from "Relaxed Memory Concurrency Re-executed" and
| suggested model looking promising
|
| - SIMD with specifically floating point NaN propagation
|
| - pointer semantics, specifically in object code
| (initialization), se- and deserialization, construction,
| optimizations on pointers with arithmetic, tagging
|
| - constant time code semantics, for example how to ensure
| data stays in L1, L2 cache and operations have constant time
|
| - ABI semantics, since specifications are not formal
|
| LLVM is also still struggling with full restrict support due
| to architecture decisions and C++ (now worked on since more
| than 5 years).
|
| > At that point though, is there really a purpose to using a
| low level language?
|
| Languages simplify/encode formal semantics of the (software)
| system (and system interaction), so the question is if the
| standalone language with tooling is better than state of art
| and for what use cases. On the tooling part with incremental
| compilation I definitely would say yes, because it provides a
| lot of vertical integration to simplify development.
|
| The other long-term/research question is if and what code
| synthesis and formal method interaction for verification,
| debugging etc would look like for (what class of)
| hardware+software systems in the future.
| eptcyka wrote:
| For constant time code, it doesn't matter too much if data
| spills out of a cache, constant time issues arise from
| compilers introducing early exits which leaves crypto open
| to timing attacks.
| matu3ba wrote:
| Thanks for the info. Do you have a good overview on what
| other hardware properties or issues are relevant?
| skywal_l wrote:
| Maybe with the new x86 backend we might see some performance
| differences between C and Zig that could definitely be
| attributed solely to the Zig project.
| saagarjha wrote:
| I would be (pleasantly) surprised if Zig could beat LLVM's
| codegen.
| Zambyte wrote:
| So would the Zig team. AFAIK, they don't plan to (and have
| said this in interviews). The plan is for super fast
| compilation and incremental compilation. I think the
| homegrown backend is mainly for debug builds.
| messe wrote:
| With regard to the casting example, you could always wrap the
| cast in a function: fn
| signExtendCast(comptime T: type, x: anytype) T {
| const ST = std.meta.Int(.signed, @bitSizeOf(T));
| const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
| return @bitCast(@as(ST, @as(SX, @bitCast(x)))); }
| export fn addi8(addr: u16, offset: u8) u16 { return
| addr +% signExtendCast(u16, offset); }
|
| This compiles to the same assembly, is reusable, and makes the
| intent clear.
| flohofwoe wrote:
| Yes, that's a good solution for this 'extreme' example. But
| in other cases I think the compiler should make better use of
| the available information to reduce 'redundant casting' when
| narrowing (like the fact that the result of `a & 15` is
| guaranteed to fit into an u4 etc...). But I know that the Zig
| team is aware of those issues, so I'm hopeful that this stuff
| will improve :)
| hansvm wrote:
| This is something I used to agree with, but implicit
| narrowing is dangerous, enough so that I'd rather be more
| explicit most of the time nowadays.
|
| The core problem is that you're changing the semantics of
| that integer as you change types, and if that happens
| automatically then the compiler can't protect you from
| typos, vibe-coded defects, or any of the other ways kids
| are generating almost-correct code nowadays. You can
| mitigate that with other coding patterns (like requiring
| type parameters in any potentially unsafe arithmetic helper
| functions and banning builtins which aren't wrapped that
| way), but under the swiss cheese model of error handling it
| still massively increases your risky surface area.
|
| The issue is more obvious on the input side of that
| expression and with a different mask. E.g.:
| const a: u64 = 42314; const even_mask: u4 = 0b0101;
| a & even_mask;
|
| Should `a` be lowered to a u4 for the computation, or
| `even_mask` promoted, or however we handle the internals
| have the result lowered sometimes to a u4? Arguably not.
| The mask is designed to extract even bit indices, but we're
| definitely going to only extract the low bits. The only
| safe instance of implicit conversion in this pattern is
| when you intend to only extract the low bits for some
| purpose.
|
| What if `even_mask` is instead a comptime_int? You still
| have the same issue. That was a poor use of comptime ints
| since now that implicit conversion will always happen, and
| you lost your compiler errors when you misuse that
| constant.
|
| Back to your proposal of something that should always be
| safe: implicitly lowering `a & 15` to a u4. The danger is
| in using it outside its intended context, and given that
| we're working with primitive integers you'll likely have a
| lot of functions floating around capable of handling the
| result incorrectly, so you really want to at least use the
| _right_ integer type to have a little type safety for the
| problem.
|
| For a concrete example, code like that (able to be
| implicitly lowered because of information obvious to the
| compiler) is often used in fixed-point libraries. The
| fixed-point library though does those sorts of operations
| with the express purpose of having zeroed bits in a wide
| type to be able to execute operations without loss of
| precision (the choice of what to do for the final
| coalescing of those operations when precision is lost being
| a meaningful design choice, but it's irrelevant right this
| second). If you're about to do any nontrivial arithmetic on
| the result of that masking, you don't want to accidentally
| put it in a helper function with a u4 argument, but with
| implicit lowering that's something that has no guardrails.
| It requires the programmer to make zero mistakes.
|
| That example might seem a little contrived, and this isn't
| something you'll run into every day, but every nontrivial
| project I've worked on has had _something_ like that, where
| implicit narrowing is extremely dangerous and also
| extremely easy to accidentally do.
|
| What about the verbosity? IMO the point of verbosity is to
| draw your attention to code that you should be paying
| attention to. If you're in a module where implicit casting
| would be totally fine, then make a local helper function
| with a short name to do the thing you want. Having an
| unsafe thing be noisy by default feels about right though.
| throwawaymaths wrote:
| you could give the wrapper function a funny name like
| @"sign-cast" to force the eye to be drawn to it.
| johnisgood wrote:
| Yeah but what is up with all that "." and "@"? Yes, I know
| what they are used for, but it is noise for me (i.e.
| "annotation noise"). This is why I do not use Zig. Zig is
| more like a lighter C++, not a C replacement, IMO.
|
| I agree with everything flohofwoe said, especially this: "C
| is clearly too sloppy in many corners, but Zig might
| (currently) swing the pendulum a bit too far into the
| opposite direction and require too much 'annotation noise',
| especially when it comes to explicit integer casting in math
| expressions ".
|
| Seems like I will keep using Odin and give C3 a try (still
| have yet to!).
|
| Edit: I quite dislike that the downvote is used for "I
| disagree, I love Zig". _sighs_. Look at any Zig projects, it
| is full of annotation noise. I would not want to work with a
| language like that. You might, that is cool. Good for you.
| codethief wrote:
| > Yeah but what is up with all that "." and "@"
|
| "." = the "namespace" (in this case an enum) is implied,
| i.e. the compiler can derive it from the function signature
| / type.
|
| "@" = a language built-in.
| johnisgood wrote:
| I know what these are, but they are noise to me.
| pyrolistical wrote:
| It is waaaaaaay less noisy than c++
|
| C syntax may look simpler but reading zig is more comfy
| bc there is less to think about than c due to explicit
| allocator.
|
| There is no hidden magic with zig. Only ugly parts. With
| c/c++ you can hide so much complexity in a dangerous way
| johnisgood wrote:
| FWIW: I hate C++, too.
| Simran-B wrote:
| It's not annotation noise however, it's syntax noise.
| johnisgood wrote:
| Thanks for the correction. Is it really not "annotation"?
| What makes the difference?
| Simran-B wrote:
| You're not providing extra information to the compiler,
| clarifying the intent, but merely follow the requirements
| of the language when writing . to infer the type or @ to
| use a built-in function.
| kprotty wrote:
| C++'s `::` vs Zig's `.`
|
| C++'s `__builtin_` (or arguably `_`/`__`) vs Zig's `@`
| johnisgood wrote:
| I hate C++, too.
| pjmlp wrote:
| Despite all bashes that I do at C, I would be happy if
| during the last 40 years we had gotten at least fat
| pointers, official string and array vocabulary types
| (instead of everyone getting their own like SDS and glib),
| namespaces instead of mylib_something, proper enums (like
| enum class in C++, enums in C# and so forth), fixing the
| pointer decay from array to &array[0], less UB.
|
| While Zig fixes some of these issues, the amount of @ feels
| like being back in Objective-C land and yeah too many uses
| of dot and starts.
|
| Then again, I am one of those that actually enjoys using
| C++, despite all its warts and the ways of WG21 nowadays.
|
| I also dislike the approach with source code only libraries
| and how importing them feels like being back in JavaScript
| CommonJS land.
|
| Odin and C3 look interesting, the issue is always what is
| going to be the killer project, that makes reaching for
| those alternatives unavoidable.
|
| I might not be a language XYZ cheerleeder, but occasionally
| do have to just get my hands dirty and do the needfull for
| an happy customer, regardlees of my point of view on XYZ.
| throwawaymaths wrote:
| the line noise is really ~only there for dangerous stuff,
| where slowing down a little bit (both reading and writing)
| is probably a good idea.
|
| as for the dots, if you use zig quite a bit you'll see that
| dot usage is incredibly consistent, and _not_ having the
| dots will feel wrong, not just in an "I'm used to it
| sense/stockholm syndrome" but you will feel for example
| that C is _wrong_ for not having them.
|
| for example, the use of dot to signify "anonymous" for a
| struct literal. why _doesn 't_ C have this? the compiler
| must make a "contentious" choice if something is a block or
| a literal. by contentious i mean the compiler knows what
| its doing but a quick edit might easily make you do
| something unexpected
| knighthack wrote:
| I'm not sure why allowances are made for Zig's verbosity, but
| not Go's.
|
| What's good for the goose should be good for the gander.
| nurbl wrote:
| I think a better word may be "explicitness". Zig is sometimes
| verbose because you have to spell things out. Can't say much
| about Go, but it seems it has more going on under the hood.
| ummonk wrote:
| Zig's verbosity goes hand in hand with a strong type system
| and a closeness to the hardware. You don't get any such
| benefits from Go's verbosity.
| Zambyte wrote:
| FWIW Zig has error handling that is nearly semantically
| identical to Go (errors as return values, the big semantic
| difference being tagged unions instead of multiple return
| values for errors), but wraps the `if err != nil { return
| err}` pattern in a single `try` keyword. That's the verbosity
| that I see people usually complaining about in Go, and Zig
| addresses it.
| kbolino wrote:
| The way Zig addresses it also discards all of the runtime
| variability too. In Go, an error can say something like
| unmarshaling struct type Foo: in field Bar int: failed to
| parse value "abc" as integer
|
| Whereas in Zig, an error can only say something that's
| known at compile time, like IntParse, and you will have to
| use another mechanism (e.g. logging) to actually trace the
| error.
| Zambyte wrote:
| Regarding the explicit integer casting, it seems like there is
| some cleanup that will be coming soon:
| https://ziggit.dev/t/short-math-notation-casting-clarity-of-...
| titzer wrote:
| Zig has some interesting ideas, and I thought the article was
| going to be more on the low-level optimizations, but it turned
| out to be "comptime and whole program compilation are great".
| And I agree. Virgil has had the full language available at
| compile time, plus whole program compilation since 2006. But
| Virgil doesn't target LLVM, so speed comparisons end up being a
| comparison between two compiler backends.
|
| Virgil leans heavily into the reachability and specialization
| optimizations that are made possible by the compilation model.
| For example it will aggressively devirtualize method calls,
| remove unreachable fields/objects, constant-promote through
| fields and heap objects, and completely monomorphize
| polymorphic code.
| int_19h wrote:
| I rather suspect that the pendulum will swing rather strongly
| towards more verbose and explicit languages in general in the
| upcoming years solely because it makes things easier for AI.
|
| (Note that this is orthogonal to whether and to what extent use
| of AI for coding is a good idea. Even if you believe that it's
| not, the fact is that many devs believe otherwise, and so
| languages will strive to accommodate them.)
| KingOfCoders wrote:
| I do love the allocator model of Zig, I would wish I could use
| something like an request allocator in Go instead of GC.
| usrnm wrote:
| Custom allocators and arenas are possible in go and even do
| exist, but they ara just very unergonomic and hard to use
| properly. The language itself lacks any way to express and
| enforce ownership rules, you just end up writing C with a
| slightly different syntax and hoping for the best. Even C++ is
| much safer than go without GC
| KingOfCoders wrote:
| They are not integrated in all libraries, so for me they
| don't exist.
| saagarjha wrote:
| > As an example, consider the following JavaScript code...The
| generated bytecode for this JavaScript (under V8) is pretty
| bloated.
|
| I don't think this is a good comparison. You're telling the
| compiler for Zig and Rust to pick something very modern to
| target, while I don't think V8 does the same. Optimizing JITs do
| actually know how to vectorize if the circumstances permit it.
|
| Also, fwiw, most modern languages will do the same optimization
| you do with strings. Here's C++ for example:
| https://godbolt.org/z/TM5qdbTqh
| Retro_Dev wrote:
| You can change the `target` in those two linked godbolt
| examples for Rust and Zig to an older CPU. I'm sorry I didn't
| think about the limitations of the JS target for that example.
| As for your link, It's a good example of what clang can do for
| C++ - although I think that the generated assembly may be sub-
| par, even if you factor in zig compiling for a specific CPU
| here. I would be very interested to see a C++ port of
| https://github.com/RetroDev256/comptime_suffix_automaton
| though. It is a use of comptime that can't be cleanly guessed
| by a C++ compiler.
| saagarjha wrote:
| I just skimmed your code but I think C++ can probably
| constexpr its way through. I understand that's a little
| unfair though because C++ is one of the only other languages
| with a serious focus on compile-time evaluation.
| vanderZwan wrote:
| In general it's a bit of an apples to fruit salad comparison,
| albeit one that is appropriate to highlight the different use-
| cases of JS and Zig. The Zig example uses an array with a known
| type of fixed size, the JS code is "generic" at run time (x and
| y can be any object). Which, fair enough, is something you'd
| have to pay the cost for in JS. Ironically though in this
| particular example one actually would be able to do much better
| when it comes to communicating type information to the JIT:
| ensure that you _always_ call this function with Float64Arrays
| of equal size, and the JIT will know this and produce a faster
| loop (not vectorized, but still a lot better).
|
| Now, one rarely uses typed arrays in practice because they're
| pretty heavy to initialize so only worth it if one allocates a
| large typed array one once and reuses them a lot aster that, so
| again, fair enough! One other detail does annoy me a little
| bit: the article says the example JS code is pretty bloated,
| but I bet that a big part of that is that the JS JIT can't
| guarantee that 65536 equals the length of the two arrays so
| will likely insert a guard. But nobody would write a for loop
| that way anyway, they'd write it as _i < x.length_, for which
| the JIT _does_ optimize at least one array check away. I admit
| that this is nitpicking though.
| uecker wrote:
| You don't really need comptime to be able to inline and unroll a
| string comparison. This also works in C:
| https://godbolt.org/z/6edWbqnfT (edit: fixed typo)
| Retro_Dev wrote:
| Yep, you are correct! The first example was a bit too
| simplistic. A better one would be
| https://github.com/RetroDev256/comptime_suffix_automaton
|
| Do note that your linked godbolt code actually demonstrates one
| of the two sub-par examples though.
| uecker wrote:
| I haven't looked at the more complex example, but the second
| issue is not too difficult to fix:
| https://godbolt.org/z/48T44PvzK
|
| For complicated things, I haven't really understood the
| advantage compared to simply running a program at build time.
| Cloudef wrote:
| To be honest your snippet isn't really C anymore by using a
| compiler builtin. I'm also annoyed by things like `foo(int
| N, const char x[N])` which compilation vary wildly between
| compilers (most ignore them, gcc will actually try to check
| if the invariants if they are compile time known)
|
| > I haven't really understood the advantage compared to
| simply running a program at build time.
|
| Since both comptime and runtime code can be mixed, this
| gives you a lot of safety and control. The comptime in zig
| emulates the target architecture, this makes things like
| cross-compilation simply work. For program that generates
| code, you have to run that generator on the system that's
| compiling and the generator program itself has to be aware
| the target it's generating code for.
| uecker wrote:
| It also works with memcpy from the library:
| https://godbolt.org/z/Mc6M9dK4M I just didn't feel like
| burdening godbolt with an inlclude.
|
| I do not understand your criticism of [N]. This gives
| compiler more information and catches errors. This is a
| good thing! Who could be annoyed by this:
| https://godbolt.org/z/EeadKhrE8 (of course, nowadays you
| could also define a descent span type in C)
|
| The cross-compilation argument has some merit, but not
| enough to warrant the additional complexity IMHO.
| Compile-time computation will also have annoying
| limitations and makes programs more difficult to
| understand. I feel sorry for everybody who needs to
| maintain complex compile time code generation. Zig
| certainly does it better than C++ but still..
| Cloudef wrote:
| > I do not understand your criticism of [N]. This gives
| compiler more information and catches errors. This is a
| good thing!
|
| It only does sane thing in GCC, in other compilers it
| does nothing and since it's very underspec'd it's rarely
| used in any C projects. It's shame Dennis's fat pointers
| / slices proposal was not accepted.
|
| > warrant the additional complexity IMHO
|
| In zig case the comptime reduces complexity, because it
| is simply zig. It's used to implement generics, you can
| call zig code compile time, create and return types.
|
| This old talk from andrew really hammers in how zig is
| evolution of C:
| https://www.youtube.com/watch?v=Gv2I7qTux7g
| uecker wrote:
| Then the right thing would be to complain about those
| other compilers. I agree that Dennis' fat pointer
| proposal was good.
|
| Also in Zig it does not reduce complexity but adds to it
| by creating an distinction between compile time and run-
| time. It is only lower complexity by comparing to other
| implementations of generic which are even worse.
| Cloudef wrote:
| Sure there's tradeoffs for everything, but if I had to
| choose between macros, templates, or zig's comptime, I'd
| take the comptime any time.
| uecker wrote:
| To each their own, I guess. I still find C to be so much
| cleaner than all the languages that attempt to replace
| it, I can not possibly see any of them as a future
| language for me. And it turns out that it is possible to
| fix issues in C if one is patient enough. Nowadays I
| would write this with a span type:
| https://godbolt.org/z/nvqf6eoK7 which is safe and gives
| good code.
|
| update: clang is even a bit nicer
| https://godbolt.org/z/b99s1rMzh although both compile it
| to a constant if the other argument is known at compile
| time. In light of this, the Zig solution does not impress
| me much: https://godbolt.org/z/1dacacfzc
| pjmlp wrote:
| Not only it was a good proposal, since 1990 that WG14 has
| not done anything else into that sense, and doesn't look
| like it ever will.
| uecker wrote:
| Let's see. We have a relatively concrete plan to add
| dependent structure types to C2Y: struct foo { size_t n;
| char ( _buf)[.n]; };
|
| Once we have this, the wide pointer could just be
| introduced as syntactic sugar for this. char (_buf)[:] =
| ..
|
| Personally, I would want the dependent structure type
| first as it is more powerful and low-level with no need
| to decide on a new ABI.
| int_19h wrote:
| This feels like such a massive overkill complexity-wise
| for something so basic.
| uecker wrote:
| Why do you think so? The wide pointers are syntactic
| sugar on top of it, so from an implementation point of
| view not really simpler.
| pron wrote:
| C also creates a distinction between compile-time and
| run-time, which is more arcane and complicated than that
| of Zig's, and your code uses it, too: macros (and other
| pre-processor programming). And there are other
| distinctions that are more subtle, such as whether the
| source of a target function is available to the caller's
| compilation unit or not, static or not etc..
|
| C only seems cleaner and simpler _if you already know it
| well_.
| uecker wrote:
| My point is not about whether compile-time programming is
| simpler in C or in Zig, but that is in most cases the
| wrong solution. My example is also not about compile time
| programming (and does not use macro:
| https://godbolt.org/z/Mc6M9dK4M), but about letting the
| optimizer do its job. The end result is then leaner than
| attempting to write a complicated compile time solution -
| I would argue.
| pyrolistical wrote:
| Right tool for the job. There was no comptime problem
| shown in the blog.
|
| But if there were zig would prob be simpler since it uses
| one language that seamlessly weaves comptime and runtime
| together
| uecker wrote:
| I don't know, to me it seems the blog tries to make the
| case that comptime is useful for low-level optimization:
| "Is this not amazing? We just used comptime to make a
| function which compares a string against "Hello!\n", and
| the assembly will run much faster than the naive
| comparison function. It's unfortunately still not
| perfect." But it turns out that a C compiler will give
| you the "perfect" code directly while the comptime Zig
| version is fairly complicated. You can argue that this
| was just a bad example and that there are other examples
| where comptime makes more sense. The thing is, about two
| decades ago I was similarly excited about expression-
| template libraries for very similar reasons. So I can
| fully understand how the idea of "seamlessly weaves
| comptime and runtime together" can appear cool. I just
| realized at some point that it isn't actually all that
| useful.
| pron wrote:
| > But it turns out that a C compiler will give you the
| "perfect" code directly while the comptime Zig version is
| fairly complicated.
|
| In this case _both_ would (or could) give the "perfect"
| code without any explicit comptime programming.
|
| > I just realized at some point that it isn't actually
| all that useful.
|
| Except, again, C code often uses macros, which is a more
| cumbersome mechanism than comptime (and possibly less
| powerful; see, e.g. how Zig implements printf).
|
| I agree that comptime isn't necessarily very useful for
| _micro_ optimisation, but that 's not what it's for.
| Being able to shift computations in time is usedful for
| more "algorithmic" _macro_ optimisations, e.g. parsing
| things at compile time or generating de /serialization
| code.
| uecker wrote:
| Of course, a compiler could possibly also optimize the
| Zig code perfectly. The point is that the blogger did not
| understand it and instead created an overly complex
| solution which is not actually needed. Most C code I
| write or review does not use a lot of macros, and where
| they are used it seems perfectly fine to me.
| quibono wrote:
| Possibly a stupid question... what's a descent span type?
| uecker wrote:
| Something like this: https://godbolt.org/z/er9n6ToGP It
| encapsulates a pointer to an array and a length. It is
| not perfect because of some language limitation (which I
| hope we can remove), but also not to bad. One limitation
| is that you need to pass it a typedef name instead of any
| type, i.e. you may need a typedef first. But this is not
| terrible.
| quibono wrote:
| Thanks, this is great! I've been having a look at your
| noplate repo, I really like what you're doing there
| (though I need a minute trying to figure out the more
| arcane macros!)
| uecker wrote:
| In this case, the generic span type is just #define
| span(T) struct CONCAT(span_, T) { ssize_t N; T* data; }
| And the array to span macro would just create such an
| object form an array by storing the length of the array
| and the address of the first element. #define
| array2span(T, x) ({ auto __y = &(x); (span(T)){
| array_lengthof( ___y), &(___y)[0] }; })
| justmarc wrote:
| Optimization matters, in a huge way. Its effects are compounded
| by time.
| sgt wrote:
| Only if the software ends up being used.
| el_pollo_diablo wrote:
| > In fact, even state-of-art compilers will break language
| specifications (Clang assumes that all loops without side effects
| will terminate).
|
| I don't doubt that compilers occasionally break language specs,
| but in that case Clang is correct, at least for C11 and later.
| From C11:
|
| > An iteration statement whose controlling expression is not a
| constant expression, that performs no input/output operations,
| does not access volatile objects, and performs no synchronization
| or atomic operations in its body, controlling expression, or (in
| the case of a for statement) its expression-3, may be assumed by
| the implementation to terminate.
| tialaramex wrote:
| C++ says (until the future C++ 26 is published) all loops, but
| as you noted C itself does not do this, only those "whose
| controlling expression is not a constant expression".
|
| Thus in C the trivial infinite loop for (;;); is supposed to
| actually compile to an infinite loop, as it should with Rust's
| less opaque loop {} -- however LLVM is built by people who
| don't always remember they're not writing a C++ compiler, so
| Rust ran into places where they're like "infinite loop please"
| and LLVM says "Aha, C++ says those never happen, optimising
| accordingly" but er... that's the wrong language.
| el_pollo_diablo wrote:
| Sure, that sort of language-specific idiosyncrasy must be
| dealt with in the compiler's front-end. In TFA's C example,
| consider that their loop while (i <= x) {
| // ... }
|
| just needs a slight transformation to while
| (1) { if (i > x) break; //
| ... }
|
| and C11's special permission does not apply any more since
| the controlling expression has become constant.
|
| Analyzes and optimizations in compiler backends often
| normalize those two loops to a common representation (e.g.
| control-flow graph) at some point, so whatever treatment that
| sees them differently must happen early on.
| pjmlp wrote:
| In theory, in practice it depends on the compiler.
|
| It is no accident that there is ongoing discussion that
| clang should get its own IR, just like it happens with the
| other frontends, instead of spewing LLVM IR directly into
| the next phase.
| kibwen wrote:
| _> Rust ran into places where they 're like "infinite loop
| please" and LLVM says "Aha, C++ says those never happen,
| optimising accordingly" but er... that's the wrong language_
|
| Worth mentioning that LLVM 12 added first-class support for
| infinite loops without guaranteed forward progress, allowing
| this to be fixed: https://github.com/rust-
| lang/rust/issues/28728
| loeg wrote:
| For some context, 12 was released in April 2021. LLVM is
| now on 20 -- the versions have really accelerated in recent
| years.
| dustbunny wrote:
| What interests me most by zig is the ease of the build system,
| cross compilation, and the goal of high iteration speed. I'm a
| gamedev, so I have performance requirements but I think most
| languages have sufficient performance for most of my requirements
| so it's not the #1 consideration for language choice for me.
|
| I feel like I can write powerful code in any language, but the
| goal is to write code for a framework that is most future proof,
| so that you can maintain modular stuff for decades.
|
| C/C++ has been the default answer for its omnipresent support. It
| feels like zig will be able to match that.
| FlyingSnake wrote:
| I recently, for fun, tried running zig on an ancient kindle
| device running stripped down Linux 4.1.15.
|
| It was an interesting experience and I was pleasantly surprised
| by the maturity of Zig. Many things worked out of the box and I
| could even debug a strange bug using ancient GDB. Like you, I'm
| sold on Zig too.
|
| I wrote about it here:
| https://news.ycombinator.com/item?id=44211041
| osigurdson wrote:
| I've dabbled in Rust, liked it, heard it was bad so kind of
| paused. Now trying it again and still like it. I don't really
| get why people hate it so much. Ugly generics - same thing in
| C# and Typescript. Borrow checker - makes sense if you have
| done low level stuff before.
| int_19h wrote:
| If you don't happen to come across some task that implies a
| data model that Rust is actively hostile towards (e.g. trees
| with backlinks, or more generally any kind of graph with
| cycles in it), borrow checker is not much of a hassle. But
| the moment you hit something like that, it becomes a massive
| pain, and requires either "unsafe" (which is strictly more
| dangerous than even C, never mind Zig) or patterns like using
| indices instead of pointers which are counter to high
| performance and effectively only serve to work around the
| borrow checker to shut it up.
| carlmr wrote:
| >requires either "unsafe" (which is strictly more dangerous
| than even C, never mind Zig)
|
| Um, what? Unsafe Rust code still has a lot more safety
| checks applied than C.
|
| >It's important to understand that unsafe doesn't turn off
| the borrow checker or disable any of Rust's other safety
| checks: if you use a reference in unsafe code, it will
| still be checked. The unsafe keyword only gives you access
| to these five features that are then not checked by the
| compiler for memory safety. You'll still get some degree of
| safety inside of an unsafe block.
|
| https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html
| dwattttt wrote:
| > the moment you hit something like that, it becomes a
| massive pain, and requires either "unsafe" (which is
| strictly more dangerous than even C, never mind Zig) or
| patterns like using indices instead of pointers
|
| If you need to make everything in-house this is the
| experience. For the majority though, the moment you require
| those things you reach for a crate that solves those
| problems.
| sapiogram wrote:
| Haters gonna hate. If you're working on a project that needs
| performance and correctness, nothing can get the job done
| like Rust.
| raincole wrote:
| I wonder how zig works on consoles. Usually consoles hate
| anything that's not C/C++. But since zig can be transpiled to
| C, perhaps it's not completely ruled out?
| jeroenhd wrote:
| Consoles will run anything you compile for them. There are
| stable compilers for most languages for just about any
| console I know of, because modern consoles are pretty much
| either amd64 or aarch64 like phones and computers are.
|
| Language limitations are more on the SDK side of things. SDKs
| are available under NDAs and even publicly available APIs are
| often proprietary. "Real" test hardware (as in developer
| kits) is expensive and subject to NDAs too.
|
| If you don't pick the language the native SDK comes with
| (which is often C(++)), you'll have to write the language
| wrappers yourself, because practically no free, open,
| upstream project _can_ maintain those bindings for you.
| Alternatively, you can pay a company that specializes in the
| process, like the developers behind Godot will tell you to
| do: https://docs.godotengine.org/en/stable/tutorials/platform
| /co...
|
| I think Zig's easy C interop will make integration for Zig
| into gamedev quite attractive, but as the compiler still has
| bugs and the language itself is ever changing, I don't think
| any big companies will start developing games in Zig until
| the language stabilizes. Maybe some indie devs will use it,
| but it's still a risk to take.
| haberman wrote:
| > I feel like I can write powerful code in any language, but
| the goal is to write code for a framework that is most future
| proof, so that you can maintain modular stuff for decades.
|
| I like Zig a lot, but long-term maintainability and modularity
| is one of its weakest points IMHO.
|
| Zig is hostile to encapsulation. You cannot make struct members
| private:
| https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
|
| Key quote:
|
| > The idea of private fields and getter/setter methods was
| popularized by Java, but it is an anti-pattern. Fields are
| there; they exist. They are the data that underpins any
| abstraction. My recommendation is to name fields carefully and
| leave them as part of the public API, carefully documenting
| what they do.
|
| You cannot reasonably form API contracts (which are the
| foundation of software modularity) unless you can hide the
| internal representation. You need to be able to change the
| internal representation without breaking users.
|
| Zig's position is that there should be no such thing as
| internal representation; you should publicly expose, document,
| and guarantee the behavior of your representation to all users.
|
| I hope Zig reverses this decision someday and supports private
| fields.
| eddd-ddde wrote:
| Just prefix internal fields with underscore and be a big boy
| and don't access them from the outside.
|
| If you really need to you can always use opaque pointers for
| the REALLY critical public APIs.
| haberman wrote:
| I am not the only user of my API, and I cannot control what
| users do.
|
| My experience is that users who are trying to get work done
| will bypass every speed bump you put in the way and just
| access your internals directly.
|
| If you "just" rely on them not to do that, then your
| internals will effectively be frozen forever.
| nicoburns wrote:
| > If you "just" rely on them not to do that, then your
| internals will effectively be frozen forever.
|
| Or they will be broken when you change them and they
| upgrade. The JavaScript ecosystem uses this convention
| and generally if a field is prefixed by an underscore
| and/or documented as being non-public then you can expect
| to break in future versions (and this happens frequently
| in practice).
|
| Not necessarily saying that's better, but it is another
| choice that's available.
| 9d wrote:
| Sure, then publish an API which gets used by huge apps or
| games, which then make thorough use of _foo and _bar, and
| now you're stuck with those fields for the next decade and
| can't change it no matter what, even though they're
| preventing a 100x performance boost that requires dropping
| those fields.
| dustbunny wrote:
| I don't care about public/private.
| 9d wrote:
| Andrew has so many wrong takes. Unused variables is another.
|
| Such a smart guy though, so I'm hesitant to say he's wrong.
| And maybe in the embedded space he's not, and if that's all
| Zig is for then fine. But internal code is a _necessity_ of
| abstraction. I 'm not saying it has to be C++ levels of
| abstraction. But there is a line between interface and
| implementation that ought to be kept. C headers are nearly
| perfect for this, letting you hide and rename and recast
| stuff differently than your .c file has, allowing you to
| change how stuff works internally.
|
| Imagine if the Lua team wasn't free to make it significantly
| faster in recent 5.4 releases because they were tied to
| _every_ internal field. We all benefited from their freedom
| to change how stuff works inside. Sorry Andrew but you 're
| wrong here. Or at least you were 4 years ago. Hopefully
| you've changed your mind since.
| haberman wrote:
| I agree with almost all of this, including the point about
| c header files, except that code has to be in headers to be
| inlined (unless you use LTO), which in practice forces code
| into headers even if you'd prefer to keep it private.
| philwelch wrote:
| > I'm not saying it has to be C++ levels of abstraction.
| But there is a line between interface and implementation
| that ought to be kept. C headers are nearly perfect for
| this, letting you hide and rename and recast stuff
| differently than your .c file has, allowing you to change
| how stuff works internally.
|
| Can't you do this in Zig with modules? I thought that's
| what the 'pub' keyword was for.
|
| You can't have private fields in a struct that's publicly
| available but the same is sort of true in C too. OO style
| encapsulation isn't the only way to skin a cat, or to send
| the cat a message to skin itself as the case may be.
| 9d wrote:
| I don't know Zig so I dunno maybe
| unclad5968 wrote:
| I disagree with plenty of Andrew's takes as well but I'm with
| him on private fields. I've never once in 10 years had an
| issue with a public field that should have been private,
| however I have had to hack/reimplement entire data structures
| because some library author thought that no user should touch
| some private field.
|
| > You cannot reasonably form API contracts (which are the
| foundation of software modularity) unless you can hide the
| internal representation. You need to be able to change the
| internal representation without breaking users.
|
| You never need to hide internal representations to form an
| "API contract". That doesn't even make sense. If you need to
| be able to change the internal representation without
| breaking user code, you're looking for opaque pointers, which
| have been the solution to this problem since at least C89, I
| assume earlier.
|
| If you change your data structures or the procedures that
| operate on them, you're almost certain to break someone's
| code somewhere, regardless of whether or not you hide the
| implementation.
| mwkaufma wrote:
| > You need to be able to change the internal representation
| without breaking users.
|
| Unless the user only links an opaque pointer, then just
| changing the sizeof() is breaking, even if the fields in
| question are hidden. A simple doc comment indicating that
| "fields starting with _ are not guaranteed to be minor-
| version-stable" or somesuch is a perfectly "reasonable" API.
| dgb23 wrote:
| Some years ago I started to just not care about setting
| things to "private" (in any language). And I care _a lot_
| about long term maintainability and breakage. I haven't
| regretted it since.
|
| > You cannot reasonably form API contracts (...) unless you
| can hide the internal representation.
|
| Yes you can, by communicating the intended use can be made
| with comments/docstrings, examples etc.
|
| One thing I learned from the Clojure world, is to have a
| separate namespace/package or just section of code, that
| represents an API that is well documented, nice to use and
| more importantly stable. That's really all that is needed.
|
| (Also, there are cases where you actually need to use a thing
| in a way that was not intended. That obviously comes with
| risk, but when you need it, you're _extremely_ glad that you
| can.)
| wg0 wrote:
| Zig seems to be simpler Rust and better Go.
|
| Off topic - One tool built on top of Zig that I really really
| admire is bun.
|
| I cannot tell how much simpler my life is after using bun.
|
| Similar things can be said for uv which is built in Rust.
| FlyingSnake wrote:
| Zig is nothing like Go. Go uses GC and a runtime while Zig
| has none. While Zig's functions aren't coloured, it lacked
| the CSP style primitives like goroutines and channels.
| timewizard wrote:
| That for loop syntax is horrendous.
|
| So I have two lists, side by side, and the position of items in
| one list matches positions of items in the other? That just makes
| my eyes hurt.
|
| I think modern languages took a wrong turn by adding all this
| "magic" in the parser and all these little sigils dotted all
| around the code. This is not something I would want to look at
| for hours at a time.
| int_19h wrote:
| Such arrays are an extremely common pattern in low-level code
| regardless of language, and so is iterating them in parallel,
| so it's natural for Zig to provide a convenient syntax to do
| exactly that in a way that makes it clear what's going on
| (which IMO it does very well). Why does it make your eyes hurt?
| csjh wrote:
| > High level languages lack something that low level languages
| have in great adundance - intent.
|
| Is this line really true? I feel like expressing intent isn't
| really a factor in the high level / low level spectrum. If
| anything, more ways of expressing intent in more detail should
| contribute towards them being higher level.
| wk_end wrote:
| I agree with you and would go further: the _fundamental_
| difference between high-level and low-level languages is that
| in high-level languages you express intent whereas in low-level
| languages you are stuck resorting to expressing underlying
| mechanisms.
| jeroenhd wrote:
| I think this isn't referring to intent as in "calculate the tax
| rate for this purchase" but rather "shift this byte three
| positions to the left". Less about what you're trying to
| accomplish, and more about what you're trying to make the
| machine do.
|
| Something like purchase.calculate_tax().await.map_err(|e|
| TaxCalculationError { source: e })?; is full of intent, but you
| have no idea what kind of machine code you're going to end up
| with.
| csjh wrote:
| Maybe, but from the author's description, it seems like the
| interpretation of intent that they want is to generally give
| the most information possible to the compiler, so it can do
| its thing. I don't see why the right high level language
| couldn't give the compiler plenty of leeway to optimize.
| 9d wrote:
| > People will still mistakenly say "C is faster than Python",
| when the language isn't what they are benchmarking.
|
| Yeah but some language features are disproportionately more
| difficult to optimize. It can be done, but with the right
| language, the right concept is expressed very quickly and
| elegantly, both by the programmer and the compiler.
___________________________________________________________________
(page generated 2025-06-07 23:00 UTC)