Furthermore, how are Rust memory allocations and layout "fairly opaque"?
In zig, you can use an arena allocator, you can free things piecemeal while using an arena, you can free the arena on program exit or in a thread, you can enforce a stack allocator, have multiple allocators, etc and because of the orthogonality of the Allocator interface, most of this knowledge is something you can probably pick up within the first 2 hours of zig (assuming you know C).
Re: edit about rust binary size. When I say zig is on par w/ C in terms of binary size, I mean I get the small binary with the basic `zig build-exe hello.zig -O ReleaseFast --strip` command, without any attempts at optimization. Do you know if this 8kb number includes the UPX optimization? If so, I'm calling shenanigans :)
I don't believe that that 8kB number is with UPX, but again, even if Rust produces smaller binaries than C, it doesn't matter. Most users would prefer faster compilation times with codegen-units over a few kB smaller binaries, for instance, as long as there's a reasonable way to get the smaller binaries for those who need them.
It's before UPX.
> I mean I get the small binary with the basic `zig build-exe hello.zig -O ReleaseFast --strip` command, without any attempts at optimization.
You could set up all these things in just a few minutes, so it's not a very big deal. If you don't mind 20KB of string formatting code then you don't have to make "any attempts at optimization" either.
So this here is sort of why I brought up binary size. I know hello world size doesn't really say anything about real world, but as I was looking at different languages, a common theme was this defensive "well what d'ya expect from a thing that has [list of features]" tone when the question of binary size got brought up and people didn't know how to make it smaller. A normal rust build has a similar feel. The 8kb binary link above reads a bit like a hacking adventure to unravel the mysteries of yonder.
When I read stuff like this
By using a C entry point (by added the #![no_main] attribute) , managing stdio manually, and carefully analyzing which chunks of code you or your dependencies include, you can sometimes make use of libstd while avoiding bloated core::fmt.
Expect the code to be hacky and unportable, with more unsafe{}s than usual. It feels like no_std, but with libstd.
I'm undoubtedly awed at the cleverness of it all, but it's clearly way off the beaten path.I should note that in addition to the zig build command being guessable by a newbie reading through --help, the hello world code itself is also the idiomatic thing a newbie would've reasonably written. And there's really no need for "air quotes", I really put no effort whatsoever in optimizing: I didn't even use `-O ReleaseSmall`
Among all the languages I looked at, Zig was somewhat unique in taking a default stance of proactively using compile time as a tool to take stuff away from the binary (in a sort of saint-exupery way).
I'm sure with enough effort it's possible to bring down binary sizes for most languages, but not many languages do this with the relentlessness that zig does out of the box.
Well, what do you expect? How much size does printf add to a Zig binary?
> When I read stuff like this
You can get the vast majority of the size savings without any of that stuff. This is someone trying to find the absolute minimum, starting with very practical suggestions before getting into itty bitty weeds. I'd suggest pretending the no_main and no_std sections don't exist.
Getting down to 50KB or 30KB is just a matter of telling the compiler to optimize for size and stripping unwind information.
If you want to keep all the unwind code and information I'm not sure how much space it takes.
> with enough effort
I will note again that the compiler settings in this repo, while they may be in multiple places, are still a very small one-time effort for a program.
So I use the size metric as a coarse proxy for "complexity required to accomplish the task at hand". And if one is to believe the mantra "to be fast, do less stuff", then my instinct is to look for technologies whose proxy metric (i.e. idiomatic hello world size) is small.
Maybe there are better ways to measure this, but so far this heuristic has worked for me.
> How much size does printf add to a Zig binary?
It depends, because comptime. Basically, it emits different code depending on the types of the params being passed in. For hello world, it wouldn't include itoa logic, for example. The irony here of course is that zig could in theory compile to a very very large binary if it had a very very large number of printf calls, all with different permutations of types. But the thing is that to a zig developer, it's clear that this is explicit static polymorphism getting compiled into a plethora of monomorphic callsites. One could implement the equivalent of go's `interface {}` underlying {type, value} tuple and have a single polymorphic function at runtime if that's what they wanted. But this same argument about how it would require someone manually implementing such a thing goes both ways: it also means that most other possible things are likely not going to be hanging around quietly adding complexity under the rug (not saying this is the case w/ rust, but it is with various other systems).
To a Rust programmer it is clear that the toolchain defaults are set up to satisfy the 99.99999999% of programmers whose goal isn't to "write the smallest hello world possible".
You can change these defaults in your system config if you don't like them.
Like you mention, one size doesn't fit all.
To me that's one of the main differences between Zig and Rust.
Most js developers' goals are indeed not to write the smallest hello world, and here we are in a world where people complain of news websites pulling MBs of scripts. As I mentioned, one could conceivably address this by merely picking jquery over react in many cases, for example. There's a predictable sliding scale of performance between using electron, nodegui and qt proper via c++. Etc. All of this is to say: Defaults and base choices matter.
I think in terms of servicing end users, rust and c++ are somewhat unique in that they are relatively complex languages targeting ambitious performance goals, at the cost of more language complexity. That has its own appeal for sure, but I echo esbuild author's sentiment about a language having to be fun for side projects, and for me personally, I like the simple "very sharp knife" variety of technologies
I'm not a Rust programmer, but I've written several projects where I'm pulling in a large Rust dependency, because the best available library for something is implemented in Rust, and it offers a C interface.
These libraries I've used have been very good, but also, extremely large. Like, over 100 MB .a files. They slow down compilation noticeably just by linking, and while a lot of the size is stripped out during linking, the resulting sizes tend to still be uncomfortably big.
So to me it seems Rust binaries tend to be huge both on the small end large ends of the spectrum, and they are large to the point where it becomes a real problem.
Nobody expects library consumers to be experts in how to build libraries.
But this isn't any different that depending on a C library, and discovering that the Makefile they are using compiles it with -O0, and makes it super slow.
To a non-C programmer, flipping -O0 to -O3 is wizardry.
---
Point being, you are not a rust programmer, and the defaults that are good for "development" are not good for "shipping" and therefore no good for you. If your library doesn't do this right, open a bug. The library can override this, and a package that builds it should override this.
Rust code is just very big. This seems to come with the territory.
As someone that uses Rust every day to program STM32s with 128KB of space... i... disagree...
Rust is the only high level language I know that allows me to customize the language run-time at will, allowing me to provide my own unwinding, threading, memory allocation, ... runtimes, to be able to program tiny machines as if I was just programming any normal one.
With other languages either I can use the standard library implementation, or I can't use the standard library and some language features at all.
With Rust, I can't use the standard library implementation for desktops, but I can just swap its backend by an embedded one, and am still able to re-use all the high level code, libraries from crates.io that use it, etc.
$ cat hello.zig
const std = @import("std");
pub fn main() void {
std.io.getStdOut().writeAll("Hello, World!\n") catch {};
}
$ zig build-exe hello.zig --strip -OReleaseSmall
$ zig build-exe hello.zig --strip -OReleaseSmall -target i386-windows
$ ldd hello
not a dynamic executable
$ strip hello
$ ./hello
Hello, World!
$ wine hello.exe
Hello, World!
$ ls -hl hello hello.exe
-rwxr-xr-x 1 andy users 1.8K Mar 10 21:15 hello
-rwxr-xr-x 1 andy users 2.5K Mar 10 21:15 hello.exe
No min-sized-zig github repository needed.From a technical perspective, my understanding is that V similar to Nim, in the sense that it compiles to C source code. That's fine and all, but IMHO, zig has a huge leg up in this area because it does first class cross compilation of C source code to binary form. To my knowledge, no other tool does this (other than maybe cosmopolitan, if you ignore everything outside posix...)
If I were to use V, I'd probably end up using `zig cc` as the `cc` for it
What's the advantage of this? Compilation speed?
Doesn't that then mean that zig can't gain compiler improvements the way using an external compiler can? For example, if C is the intermediate language for $HIGH-LEVEL-LANGUAGE, then $HLL benefits every time that the specific C compiler being used is upgraded.
Zig uses LLVM under the hood so it does benefit from LLVM upgrades. Also, there's devils in the details. For example, the copy ellision thing is something that required deliberate implementation; it doesn't just come for free if you're naively emitting C.
> For example, the copy ellision thing is something that required deliberate implementation; it doesn't just come for free if you're naively emitting C.
I don't know what copy elision is. Can you explain that too?
> Most objects (~90-100%) are freed by V's autofree engine: the compiler inserts necessary free calls automatically during compilation. Remaining small percentage of objects is freed via reference counting.
> The developer doesn't need to change anything in their code. "It just works", like in Python, Go, or Java, except there's no heavy GC tracing everything or expensive RC for each object.
So how does V handle reference cycles? A partial solution based on static-analysis (autofree) isn't going to catch all reference cycles, by definition, and automatic reference-counting won't catch reference-cycles either.
I have to agree with lhorie's comment: I get the impression I'm only getting half the story.
The Nim folks also often seem to skim over the question of reference cycles.
So, in this case the skimming is just an "abbreviated story" which seems reasonable when the full story is long (the Nim project has been around since before 2008...).
For V, well it did have a huge initial hype/propaganda cycle and is also quite young, and I believe the V author made a bunch of extreme claims before publicly releasing the code for scrutiny/corroboration. So, that may be much more suspicious.
[1] https://nim-lang.org/blog/2020/12/08/introducing-orc.html
From your linked article:
> it turns out there are lots of ideas that the GC research overlooked. Exciting times for Nim!
I wish them luck, this is a field where great progress has been made over the last decade, hopefully more to come. If they really can beat the JVM head-on, that would be very impressive, especially if they do so while using C as an intermediate language.
The one time when I had trouble with Nim's GC perf was years ago implementing Wolfe Garbe's Symmetric Delete spell checking algorithm [0]. Even then, it worked with nim c --gc:boehm but blew up with others. So, there was nothing stopping me from doing it in Nim. This was my initial "in volatile RAM" solution before I went to a fully persistent on-disk memory mapped solution [1]. SymDel is an algorithm particularly demanding of prog.lang run-time basics as explained in that link. I've come to think of it as a good stress test. { Not the only one, of course :-) } I wonder how well Java's GCs would fare.
I often refer to zig as a "very sharp knife": it's "cool" for new languages to have more safeguards to protect you from yourself, but Zig feels a bit like it goes in opposite direction in the sense that it exposes the underlying plumbing more than most languages. For example, Go and Rust memory allocations and memory layout are fairly opaque; in zig you can control it idiomatically with obsessive precision.
But unlike C, zig offers a host of safety features, like integer overflow checks, compiler-checked optionals and exhaustive switch, as well as a well behaved compile-time system, and a bunch of syntactical sugar ("method" syntax, if/while/block/return expressions, etc).
And unlike C++, zig is a very small language.