While learning the language, I tried to follow as closely as possible all the advices on the 0.16 documentation, but from the discussion above it seems that it is not quite updated.
When is it advisable passing a struct via value or reference? Coming from C++, I was prepared to pass everything by reference (apart from basic types), but looking at the documentation and some other threads, it looked like the “Zig Way” was to just default to pass by value and let the compiler decide what to do.
For example if I have a struct that holds the allocator and IO interfaces, should I pass it by value or reference? I ask because each of these interfaces will hold a bunch of pointers, of non-negligible size.
zig compiler no longer chooses to pass-by-ref for you as the implementation was flawed. I dont know if the language docs just haven’t been updated, or if they are reserving the right to re-implement it in the future.
if you dont need a pointer for other reasons, default to passing by value. If it is an issue you will notice when benchmarking, if you’re not benchmarking then it wont be an issue.
Thanks. Yes, I was defaulting to pass by value as you suggested. I am just trying to better understand the unwritten rules of the language, since it helps to write better code in general (without necessarily having to put it on godbolt every time).
if you’re not benchmarking then it wont be an issue
I am just hesitant of writing/learning in a reckless manner and having the performance penalty to accumulate over the codebase. Then it’s much harder to profile everything than just a few bottlenecks.
I’m not sure if parameter reference optimization will come back, and I’m also concerned about potential performance loss.
So I replaced some pass-by-value cases, which originally relied on parameter reference optimization to express certain semantics, with noalias arg: *const T.
I’m hoping that parameter promotion can optimize them as pass-by-value, and in the future, if parameter reference optimization comes back, it will be easy to grep them and change back to pass-by-value.
I’m trying to think - is there much more to it than this rule?
I’ve always had it in my head that this value vs. reference thing had all kinds of complicated platform specific edge cases, but I can’t think of cache locality arguments or any tricky stuff like that off the top of my head…
The problem really isn’t the specific calling convention of the platform.
The problem is interaction with optimization passes.
Zig “Pass by Value” will copy big struct on the stack and pass a *const noalias T to the function. If the callee get inlined then the memcpy disappears and all is well. But it puts pressure on the inliner and the extra memcpy make the functions harder to inline. Each of those copy is very fast and is unlikely to appear in a profiler, but aggregated they are pesky.
*const T seems like the easy solution, but it disable a lot of LLVM optimizations because compilers can’t reason about memory aliasing. And a lot of the Std generic function like HashMap, only deal with values because Zig used to be more aggressive here.
I’m not happy with the status quo, but there is no indication that a better solution is possible nor that the core team is considering this to be a problem.
If someone cares to open a brainstorming thread, we can gather some ideas