Both programs do the identical workload: connect to X server, create a 400×400 window, create 2 graphics contexts, query text extents for 'm', map the window, and on Expose draw a filled rectangle + clear an area + draw "Hello X!".
examples/hello.zig— zigx version (pure Zig, no C deps)examples/hello_xcb.zig— libxcb version (@cImport("xcb/xcb.h"), links libc + libxcb)
Both built with Zig 0.15.2 on Linux x86_64. Benchmark mode triggered via BENCH=1 env var (exits after first render).
| Mode | hello (zigx, static) | hello_xcb (exe only) | hello_xcb + shared lib deps |
|---|---|---|---|
| Debug | 13.8 MB | 7.1 MB | ~10.0 MB |
| ReleaseSafe | 3.4 MB | 2.2 MB | ~4.8 MB |
| ReleaseFast | 4.1 MB | 1.3 MB | ~3.9 MB |
| ReleaseSmall | 154 KB | 18 KB | ~2.6 MB |
Shared lib deps for hello_xcb: libxcb (162 KB) + libc (2.1 MB) + ld-linux (237 KB) + libXau (19 KB) + libXdmcp (27 KB) ≈ 2.57 MB.
zigx is a fully static ELF with zero dynamic dependencies. libxcb requires 6 shared libraries at runtime.
| metric | hello (zigx) | hello_xcb | overhead |
|---|---|---|---|
| VmRSS (physical) | 196 KB | 1,920 KB | 9.8× |
| RssAnon (heap+stack) | 40 KB | 180 KB | 4.5× |
| RssFile (mapped files) | 156 KB | 1,740 KB | 11.2× |
| VmLib (shared-lib code) | 8 KB (vDSO only) | 1,924 KB | 240× |
| VmData (data+heap virt) | 28 KB | 260 KB | 9.3× |
| metric | hello (zigx) | hello_xcb | delta |
|---|---|---|---|
| wall_time | 21.6 ms | 24.2 ms | +12% |
| cpu_cycles | 65.5K | 451K | +589% |
| instructions | 47.6K | 392K | +724% |
| cache_references | 5.14K | 34.5K | +571% |
| cache_misses | 2.56K | 11.8K | +361% |
| branch_misses | 831 | 6,290 | +656% |
| peak_rss | 291 KB | 1.90 MB | +552% |
zigx does ~8× less CPU work. Wall-clock gap is only 12% because both are latency-bound on X server round-trips.
| hello (zigx) | hello_xcb | |
|---|---|---|
| total syscalls | 65 | 118 |
| distinct syscalls | 14 | 30 |
zigx only issues X-protocol syscalls (socket, connect, sendmsg, writev, readv, shutdown, close) plus execve boilerplate.
hello_xcb adds ~53 extra syscalls before main() runs, all from ld.so loading shared libraries:
| syscall | count | why |
|---|---|---|
| mmap | 30 | map each .so into memory |
| openat | 8 | open each .so file |
| fstat | 8 | inspect each .so |
| mprotect | 8 | set page permissions |
| pread64 | 2 | read ELF headers |
| brk | 3 | glibc malloc arena setup |
| plus | ~10 | getrandom, rseq, set_tid_address, set_robust_list, fcntl, uname, getsockname, getpeername, access, etc. |
- Both files are ~130 lines of Zig.
hello.zig(zigx): pure-Zig protocol implementation, no C interop.hello_xcb.zig:@cImport("xcb/xcb.h")+ linkslibc+libxcb.
| dimension | winner | margin |
|---|---|---|
| Exe size on disk (just the binary) | libxcb | 1.5–8× smaller |
| Total deployed bytes (exe + required libs) | zigx | 3–17× smaller (esp. ReleaseSmall) |
| Startup CPU cost | zigx | ~7× (instructions), ~7× (cycles) |
| RSS / memory footprint | zigx | ~10× |
| Syscall count | zigx | ~2× |
| Wall-clock (this workload) | zigx | only ~12% (latency-bound) |
| Deployment simplicity | zigx | single static binary, no .so deps |
For short-lived X11 utilities that draw minimal UI, zigx is dramatically cheaper across every dimension except "just the executable file size on disk" — and even that flips the moment you count the shared libraries libxcb requires at runtime.
The performance & memory gap shrinks for long-running programs, since most of libxcb's cost is one-time dynamic-linker + libc setup. But the 154 KB fully-static ReleaseSmall binary is a capability libxcb simply cannot match.
Measurements done on:
- Linux 6.17.0-19-generic, x86_64
- Zig 0.15.2
- libxcb 1.15+ (Ubuntu system package)
- Xorg server (
/usr/lib/xorg/Xorg)
Build commands:
zig build build-hello build-hello_xcb -Doptimize=ReleaseSmall
zig build build-hello build-hello_xcb -Doptimize=ReleaseFast
# etc.Memory measured via awk '/^Vm|^Rss/' /proc/<pid>/status after window appears.
Performance via poop, 5-second duration, requires sudo -E or sysctl kernel.perf_event_paranoid=1:
BENCH=1 sudo -E poop -d 5000 ./zig-out/bin/hello ./zig-out/bin/hello_xcbSyscalls via:
BENCH=1 strace -c ./zig-out/bin/hello
BENCH=1 strace -c ./zig-out/bin/hello_xcb