[Idea] Returning unsized locals via stack

Good catch.

Under the RFC it's probably possible to ensure values
both of compile-time known and compile-time unknown sizes
are always laid out in the same order on stack
regardless of which execution path through its code an fn takes.

This indeed means the "final" callee can shift values up the stack trivially
and no complex rules are needed to ensure the objects it returns
are laid out on stack in the same order in which pointers to them are returned.

But

Are there reasons - apart from this CC - not to permit code as in my example?
E.g. are there other reasons for the RFC not to change?
Not to become a little more permissive?
And would that not be a good change to the RFC?

And

fn f1() -> (dyn Trait, dyn Trait) {
   let a = ConcreteType1{...}
   let b = ConcreteType2{...}
   // this fn knows the relative ordering of a, b 
   // on the stack and can shift them trivially
   return (a, b);
}

fn f2() -> (dyn Trait, dyn Trait) {
   let (a, b) = f1();
   // this fn no longer trivially know if a or b come
   // first on the stack and cannot shift them trivially
   return (a, b);
}

@atagunov, I see your points about squeezing every last drop of performance out that you can, but I strongly prefer @tczajka's points about letting the compiler handle things as far as possible for the following reasons:

  1. Very few average rust programmers are as good as the average programmers on this forum. Adding additional rules that in theory improve performance, but which require a deep understanding of the rules like this will likely mean that there will be lots of buggy code out there, or lots of questions (and grumbling) on various forums about this feature when the compiler starts kicking out errors over its usage.
  2. The easier a feature is to use, the more often it will be used, which leads to faster code overall. If I, as a working programmer, know that I have to track all these rules to even begin to optimize my code, I'm less likely to use the feature simply because of the mental overhead of writing and maintaining the code. On the other hand, if the compiler handles the complexity for me, I'm far more likely to use the feature in as many places as I can. So, while any single location may be less optimal than what could be done via hand-coding, the overall result may be that the compiler-optimized code is faster (caveat being that the most time-critical path is slightly suboptimal).
  3. The compiler keeps improving non-stop. That means that every time the optimizer is improved to catch more patterns related to this feature, the better its code gets. If I want greater performance, then I can try to use a newer compiler. This will be true for years to come. If I have to remember the rules and apply them correctly each time I update a chunk of code, then the compiler is relying on me and my programming skills to be better than the compiler's abilities.

In short, while I agree that faster code == better code, it has to be balanced against overloading the programmer.

Please don't take any of this as being against this feature! I like it a lot, and really want to see it go forwards as I see it as a first step to relaxing some of the weirder rules around trait objects, which will mean that they can be used more easily, and in more places. The only thing I'm asking is that the compiler be the one responsible for enforcement of the rules, and for optimizing code, not the programmer.

2 Likes

@ckaran, thank you for your encouragement :slight_smile:

  • your points are valid, however I'd like to note

  • the "weird" rules only matter when more than 1 dyn object is returned

  • I certainly don't mind some kind of explicit mechanism like

    // return dyn objects not in the order they are laid out on the stack
    return reorder!(a, b, c);
    
  • I am also wondering if the rules could somehow be made more comprehensible

  • I may or may not find time to do practical tinkering, but others - yourself included - are certainly free to adjust my designs however they like should they reach approval/implementation stage

You're right! But that brings up the mental overload problem I mentioned earlier. I've been programming in Rust since about rustc 1.19, and I've been a professional programmer for about 20 years now. I still have to look up the rules for how to use trait objects correctly, and even then it usually takes me a fair amount of poking, trying, and 'creative language use' :wink: before I get it right. That's because a) I don't use them very often, and b) there are just enough rules surrounding them that I forget one or two between the times I use them.

If your proposal is adopted as-is, requiring the programmer to learn more rules, then I expect the average programmer will be stuck in the same limbo that I'm in; they know it's useful, they've used it successfully in the past, but they don't use it often enough to really remember the rules, and so they spend lots of time trying to satisfy the compiler. Let the compiler do the heavy lifting here, and save the mental loading for something where the compiler really can't do it on it's own.

What about turning it around, and using something more like manually_reorder!()? If you don't specify the reordering, then the compiler handles it for you, but if you use manually_reorder!(), then you're explicitly telling the compiler the order you want to use, and telling it not to change it. This could be a compiler hint, similar to black_box.

You deserve it, it's a good idea!

Ok the manual/automatic reordering is one point that would need to be resolved if this ever goes forward.

Even bigger points to be resolved - and probably before the manual/auto one are

  • do we feel it's indeed necessary to require that - though whichever mechanism - values are laid out on stack in the order pointers to them are returned?

  • which is the right party to do stack shifting

    • always the callee
    • always the caller
    • can be chosen at callsite
      • if the fn is exported from a .dll/.so
        • is the choice done via an extra parameter
          • not part of signature as seen from inside Rust
          • part of fn signature as seen from inside Rust
          • parameter is part of signature as seen from C
          • parameter is "hidden" even from C
            • like for example the odd bit on the address of sret memory buffer (but what if return values all go in registers? then we don't pass the address for the sret memory buffer)
            • some scratch register that we don't normally use for fn parameters
        • are two different fn-s exported
          • do they share any code internally?
        • smth else

P.S. this train of thought had been inspired by Swift ABI stability guarantees, Swift's choice not to monomorphize and Swift's preference to make the compiled code compact. I was trying to explore ways to export generic functions from .dll/.so-s without monomorphization. Hence the accents. Ideally the resulting CC would be usable both in and outside of Rust :slight_smile:

My vote is that the callee be responsible in all cases. If the caller wants to do some reordering afterwards, then its the caller's responsibility. The will make for a much cleaner interface.

I also just thought of a way of forcing stack ordering that might be more ergonomic. Change rust's syntax slightly so that you can do something like the following:

fn foo() -> (a: dyn Trait, b: dyn Trait, dyn Trait, dyn Trait, usize) {
// `a` and `b` are already bound, but uninitialized
}

When you return something from a function, all named traits must be before all unnamed traits, which must be before all Sized objects. The named objects are already on the stack, in the right order for being returned, but all are uninitialized (like MaybeUninit::uninit), and the compiler is aware of this. Within the body of the function, both a and b must be given values along all possible code paths, effectively initializing them. I think this is more or less just sugar for an anonymous struct whose definition is shared between the caller and callee, with the unnamed fields being given compiler generated names (I know nothing about compiler internals, take that with a massive grain of salt), so implementing it shouldn't be too hard...

I see a few advantages to this method:

  1. The ordering becomes a part of the API. If you want to change the ordering of a public function that has promised to return trait objects in a particular order, then you need to obey the semver requirements and bump your version number. That forces programmers to really think about their ordering before they start changing things. It also means that we can write clippy lints to catch when we change ordering in public APIs, but fail to bump the major version number.
  2. Fairly ergonomic. This looks very similar to how you define the parameters to an argument, so while the position requirements will feel strange (AFAIK, you can reorder trait objects however you feel like as parameters), an average rust programmer should be able to guess that we're returning the objects named a and b, and in that order.
  3. No magic macros or attributes, etc. It's just syntax.

I recon the issue here is probably that a and b can themselves be unsized locals. Each unsized local is effectively an alloca of a size possibly not known at compile time. Like a and b in f2() in my post no. 21. The only way I can imagine the compiler can place them on the stack is in the order in which they are assigned to, not declared.. Yes it can later copy them around using more stack, which I understand is the essence of the "automatic" suggestion.

Other than that - while I certainly see the advantage of giving names to tuple members - it does look like an orthogonal change. So I would expect it to be discussed and implemented independently.

Because of these considerations I believe that the syntax I suggested originally still makes sense:

fn foo() -> (dyn Trait, dyn Trait, dyn Trait, dyn Trait, usize) {
    // here a, b, c, d can be concrete types
    // they can also be unsized locals
    // it seems like a nice property to have that when foo returns
    // objects they point to are laid out on the stack in the order
    // a, b, c, d

    // indeed ordering is then part of foo()'s interface

    // this can be achieved either via complicated coding discipline (manual) or
    // by compiler taking control and possibly copying things around (automatic)
    .... return (a, b, c, d, e);
}

BTW apart from dyn Trait and dyn T[] I wanted to be able to return dyn ConcreteType. This facility thus would become an alternative to traditional way of returning data. This would make sense in "caller does stack shifting" case. A lot less so in "callee does the shifting" case.

What if the compiler enforces the order of assignments based on the order of the return parameters? That is, a has to be assigned to before b, then the unnamed Unsized values, then anything that is sized?

I'd say that is similar in spirit to the rules I was trying to set out say in post no. 11
I actually think that spiritually that is the right approach
I favor it over automated ones

Nothing is set in stone of course. Should this line of work miraculously move into mainstream one day it won't be me determining auto vs manual :slight_smile:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.