Zaggle: optimizing Mandelbrot

So i was taking my waggle project (a tiny graphics lib that depends on MiniFB only, didn’t go too far) to Zig (zaggle)

I had a Mandelbrot example, thought’d be a great thing to optimize.

The underlying implementations are not really changed, so performance should be similar.
But what I got when benchmarking was this:


(data from waggle are labled with c_, while zaggle is not labeled;
baseline for naively iterating, 4thrd for tiling 4 thread implementation, 4async for tiling 4 std.Io.async implementation;
C version built with -O3, Zig version built with -Doptimize=fast)

You can see that the Zig baseline outperforms the C baseline, even close to 4 threaded C.

I heard that Zig might be faster than C, that it can do more optimization tricks, but didn’t expect this much tbh

Also, here’s the mandelbrot code for both, 'd be grateful if anyone points out any wrong places / gives optimization stratagies.
suxyio/zaggle examples/src/mandelbrot_perf_log.zig
suxyio/waggle examples/mandelbrot.c

Did you compile the C code with Zig, and/or -march=native? And if your C code is spread across multiple source files, are you building with -flto to enable optimization across compilation units?

AFAIK these are the two main differences between C and Zig’s build models. Another smaller issue might be more aggressive optimizations around pointer aliasing in Zig vs C.

But in general, giving the same memory access patterns (e.g. same code structure and data types) and giving the C compiler the same aggressive compilation options that Zig uses by default, I would except nearly byte-by-byte identical output (assuming both compilers are LLVM based) - and that should be true for all compiled languages btw, not just Zig vs C.

Or maybe when on x86 you’re lucky and you hit a sweet spot in Zig’s non-LLVM backend :slight_smile:

PS: looking at the code, this may already be the culprit:

https://codeberg.org/SuXYIO/waggle/src/commit/b2856d5e23a59560697148b8b922fdfaa5726066/examples/mandelbrot.c#L71

… it looks like you’re calling a callback for each pixel from within equa_draw_4thrd, and I guess that function lives in another C source file.

In Zig the per-pixel callback function body will almost certainly be inlined into the caller because Zig builds the entire program as a single compilation unit, while in C it definitely won’t be inlined unless you build with -flto (and even then it’s not guaranteed)… and this inlining may in turn unlock more optimizations (like loop unrolling or vectorization)…

2 Likes