Show HN: Tiny 1k Rust binary on Windows
github.com
github.com
You can link libc statically and dead-code-eliminate everything except print. That's what Rust does too, but Rust has fancier machinery for formatting and handling of panics with backtraces. These bits don't get dead-code-eliminated as thoroughly in Rust (probably because the print could theoretically fail).
And below libc is probably X or Wayland which tells the kernel where to put pixels. And below that is the kernel and graphics driver which figures out how to do that and communicates it to the GPU. And below that is the actual GPU which figures out how to modulate an array of pixels into an HDMI signal. When do we stop quibbling about the size of binaries? It’s pointless to point out that small code relies on other abstractions. No one complains about tiny demo scene programs using tons of other stuff built into the OS. So why here?
C64 demos and 8088 DOS demos use very little OS infrastructure. Obviously things like JavaScript demos do.
The challenges presented by a particular piece of hardware or software in the demo scene are chosen for different reasons.
Sometimes, the demo author wants to show how well they can use the existing OS infrastructure. Sometimes they want to show what can be done without it. In both cases, small sizes are impressive.
It's just worth knowing when you compare exe sizes that C doesn't have an order of magnitude more efficient code generation. It just had a >30-year head start to get its standard library shipped with every operating system. Other languages typically don't have that advantage, and either have to reuse libc, or take a hit from statically linking their own libstd.
In your example, both the C bin and Rust bin are using X, GPU, etcetc - so they warrant no distinction. So what is different? What is included in the Rust binary that isn't in the C? Because that's what is being tested in these types of comparisons.
This difference may be because some language included inefficient instructions - has bloated includes, etc. Or it could be because you're comparing Apples to Oranges, eg one ships with libc in the binary, and one dynamically links to it.
Again: it was just a rant.
So it's Apples to Oranges because they're fundamentally doing different things, and yet we're comparing their Line Count. That's not only reasonable, it's necessary if you want an Apples to Apples comparison.
That's why we're not concerned about the code to render with GPU or some other junk - but we are fundamentally talking about two programs that do different things - but then comparing their line count.
Your rant may be valid in other cases, but it seems super off base here. Your rant has, imo, nothing to do with what's happening in people pointing out the discrepancy.
int main(void) {
puts("Hello World!");
return 0;
}
The Rust version is a mess [0][0] https://github.com/mcountryman/min-sized-rust-windows/blob/m...
#include <windows.h>
int _start() {
const char str[] = "Hello world!\n";
HANDLE stdout = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD written;
WriteFile(stdout, str, sizeof(str), &written, NULL);
ExitProcess(12);
return 0;
}https://stackoverflow.com/a/42536990
Which is similar to what the Rust program does. The result should be significantly less than 1k
cl small2.c /MD /link /entry:_start /subsystem:CONSOLE kernel32.lib
It's 5kb.https://docs.microsoft.com/en-us/cpp/build/reference/nodefau...
cl small2.c /link /force /nodefaultlib /entry:_start /subsystem:CONSOLE kernel32.lib
This gets it down to 3kb. #include <stdio.h>
int main(void) {
puts("Hello World!");
return 0;
}
I then compiled it using this command: cl small.c /MD
The result? 9kb! That's 9 times the size.Compiling that source on Windows using visual studio produces a 98K executable.
/MD /link /align:4096 /filealign:512 /merge:.rdata=.text /merge:.data=.text
That produced a 1.5K binary, which with a custom stub should reach 1K.> LINK : warning LNK4254: section '.data' (C0000040) merged into '.text' (60000020) with different attributes
text data bss dec hex filename
1566 600 8 2174 87e hello
(no optimisations) println!("Hello world!");
Exactly that text. Compile it and it comes in at exactly 13.6% of the C program you have that compiles to 1k.[0] https://github.com/mcountryman/min-sized-rust-windows/commit...
That first PR moved the old asm to llvm_asm, and then the new asm was built after a period of time so folks could migrate their code to llvm_asm, as a temporary measure. This commit happened after that transition so this has to be from the new asm back to the transition version.
I think (?) its compiler sort-of implements a more advanced version of Unix strip.
Andrew Kelley could probably chime in more on it.
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, world!\n", .{});
}
----------zig build-exe hello.zig -O ReleaseSmall --strip --single-threaded -target x86_64-windows
Resulting hello.exe is 3072 bytes. From these 2560 are zero bytes. The only system calls from the resulting binary are to ExitProcess, GetLastError and WriteFile from KERNEL32.dll Additionally, changing the release optimizations:
zig build-exe hello.zig -O ReleaseFast --single-threaded --strip -target x86_64-windows
produces hello.exe of 2560 bytes, 1849 from which are zeroes.
Note all: winter_blue has right to mention Zig. It's about what's provided out of the box in that new language and the provided optimizations are really effective.
Edit: asking mcountryman to explain: "the compiled rust exe has 480 non-zero bytes" -- I think you mean not just "compiled" but "hand-optimized writing hundreds of lines of the rust source code and not using standard library"? https://github.com/mcountryman/min-sized-rust-windows/blob/m... https://github.com/mcountryman/min-sized-rust-windows/blob/m...
Edit2: Please note that zig doesn't eventually link to printf and libc there, but links and resolves the std calls from the source to the calls to KERNEL32.dll.
The 100 line hack I used was to noop out the `.idata` section which link.exe refused to merge/shrink, that only saved me 1Kb in the end.
Looking at the section info of the zig exe it looks like there is some black magic going on with it's alignment so I may have to dig deeper to see what zig is doing here. Also thank you both for pointing zig out, I haven't heard of it until now and it may come in handy for something else I'm planning on making :)
std/debug.zig:
pub fn print(comptime fmt: []const u8, args: anytype) void {
const held = stderr_mutex.acquire();
defer held.release();
const stderr = io.getStdErr().writer();
nosuspend stderr.print(fmt, args) catch return;
}
io/writer.zig: pub fn print(self: Self, comptime format: []const u8, args: anytype) Error!void {
return std.fmt.format(self, format, args);
}
std/fmt.zig: pub fn format(
writer: anytype,
comptime fmt: []const u8,
args: anytype,
) !void {
...
It's all based on the language features and the actual implementations of zig's standard library code, and at least I don't see anything "wrapped" there, and nothing what C++, C or asm programmers understand as "a macro".I didn't actually check the zig std source. I was referring to what I was seeing in radare2 :)
https://ziglang.org/documentation/master/#comptime
See an example calling firstNPrimes. "When we compile this program, Zig generates the constants with the answer pre-computed." "Note that we did not have to do anything special with the syntax of these functions. For example, we could call the sum function as is with a slice of numbers whose length and values were only known at run-time."
Then see "Case Study: printf in Zig"
These literally just contain the compiled assembly and are usually used on, for example, the 4k demoscene.
1k with rust is certainly a nice squeeze.
And yes, the com extension is still recognized and will pop up a dialog telling you "This app can't run on your PC".
But actually .com and .exe files are handled equivalently, so you can just rename an .exe (i.e. PE executable) to .com and it will run - that doesn't make it a COM executable though...
There are plenty of 1k demos too, on Windows and otherwise:
Edit: Also I believe ntdll doesn't export `GetStdHandle` anymore.
[0]: https://processhacker.sourceforge.io/doc/struct___r_t_l___u_...
[1]: https://docs.microsoft.com/en-us/windows-hardware/drivers/dd...
https://i.imgur.com/ErHU7Kr.png https://github.com/mcountryman/min-sized-rust-windows/commit...
Can you test it and see how it behaves with msvc toolchain?
My bad everyone, my bad!