What is the current status of return value copy elision?

AFAIK we had this merged PR: result location mechanism (part of no-copy semantics) by andrewrk · Pull Request #2602 · ziglang/zig · GitHub

But when I experimented with this:

fn initSomething() !Something {
    var ret: Something = undefined;
    printAddress(&ref);
}

var a = initSomething();
printAddress(&a);

I see different addresses.

What am I missing?

Well first of all this line here:

var a = initSomething();

This line create a copy of the result location, because the result location is readonly and therefor cannot be stored in a var(see also why is the machine code different for these two different function calls? · Issue #16368 · ziglang/zig · GitHub). Note that this is trivial for llvm to optimize and should only be visible in debug.
If you did

const a = initSomething();

then the result location would actually be stored directly in a.

And secondly I think the result location is only used for the return statement and not forwarded to the variable that gets returned. So I think you’ll only observe it when you do something like this:

return .{...};

But then you cannot print the address of this. So you are trying to observe something that doesn’t want to be observed. I think you’d need to check the generated air or llvm-ir or assembly code.

Oh and also recently there was a change that limited the scope of RLS, disabling it for initializers with explicit type T{...}: Proposal: do not perform RLS on explicitly typed struct and array inits · Issue #17194 · ziglang/zig · GitHub

2 Likes

@IntegratedQuantum haha. Sounds like quantoms! If you don’t observe it then it’s uncertain!

I think the best practice for now is to forget about copy value elision until Zig reaches 1.0

3 Likes

Did anybody check the assembler? Developing for embedded system and every cycle counts.

I would like to expand on the question, I see a lot of places for template functions where an init method does not reference self, but rather:

pub fn init() TheType(parameters) {
  return .{
     // initialize members
  }
}

If I change this to:

// pseudo-code
const res : TheType(parameters...) {
  // initalize
}

std.debug.print("buffer ptr {*}", .{&res._buffer});

I then check the buffer pointer in the calling function… and it differs; which in my simple mind mean that, as @IntegratedQuantum explained, that it gets copied.

So the question is:

  1. Does Zig magically elision if not using a temporary const, or does it always copy (the same question as being asked already)?
  2. For embedded systems, is it not then better to have the init as a method of the struct (see below)
  3. When allocating the struct on the heap vs stack, does the self.init not also have more value over the “static” init that copies the value?
var instance : TheType(parameters) = undefined;
instance.init(param1, param2)

I ask these questions to learn, as I see a lot of init that returns a struct, which may be detrimental?

Be careful here, there is likely another mechanism at play:
const declarations with comptime-known initializers are also comptime-known. In this case the value will live in the readonly section of your binary, and of course the pointer value is different.

Note that you can enforce this behavior if you make init a constant instead of a function.

From my understanding (though it may be different since the last time I looked at it) in the llvm IR that Zig emits it will always make a copy if the result is a var. However the llvm optimizer can (and often enough will) remove the copy operation.

This is the type of thing that you shouldn’t worry about unless it actually shows up in your profiler.

Thanks for the reply. The basis of the question is also surrounding the pattern. Do you have any thoughts on that as -

one of the basic Zig fundamentals are control and transparency (very little magic that blindsides you), at least in my understanding,… In other words, would like to take the most predictable path for the implementation pattern followed.

I have also had my share of issues on copies of structs that create incorrect references; a problem which goes away when not copying, apart from the chance that the object is quite big and not all that quick to copy. I also cannot ignore this, as said, working on stuff with very little resources.

Will get around to doing a benchmark sometime; which sometimes can be very surprising depending on platform… sometime soon.