When I program in C++, I often like to break up long functions by introducing local functions in the form of lambdas, especially when code fragments need to be repeated. I prefer local helper functions because they don’t pollute the parent’s namespace and are directly proximate to their uses. I’ve always trusted optimising compilers to look through what look like function calls at the language level but are really ways to inline code.
So I do miss direct language support for lambdas in Zig. But Zig does offer local static functions, so I put a simple one into Compiler Explorer to see if they are optimised similarly. And then I discovered something had changed for the better since 0.13.0, but I’m not sure what part of the compiler is responsible for the improvement.
export fn square(num: i32) i64 {
const S = struct {
pub fn op(a: i32, b: i32) i64 {
return a * b;
}
};
return S.op(num, num);
}
generates the following assembly:
square:
mov eax, edi
imul eax, eax
ret
which is perfect. So I wanted to compare it to the non-lambda version:
export fn square2(num: i32) i64 { return num * num; }
but I was outsmarted by Zig trunk:
square2:
jmp square
In Zig 0.13.0, we get
square2:
mov eax, edi
imul eax, eax
ret
I realise this is not earth-shattering, but I thought it was interesting that there’s some compiler optimisation introduced between 0.13.0 and trunk that realises square
and square2
are effectively the same set of operations. Possibly this is an LLVM improvement?
Here’s the godbolt link: Compiler Explorer
Now I can be care-free creating heaps of local functions. I don’t like the extra level of indentation caused by the wrapping struct, but I do like the optimisations.