Pre-RFC: become-assignments for reliable RVO/DPS

Motivation

RVO/DPS is an optimization that avoids allocating unneeded stack temporaries in assignments. For example, in the C code

#include <stdlib.h>
struct big { int data[1<<30]; };
void poke(struct big *b);
void foo() {
    struct big *s = malloc(sizeof(*s));
    *s = (struct big){{[163]=42}};
    poke(s);
}

translates into this assembly code:

    pushq    %rbx
    movabsq    $4294967296, %rdi
    call    malloc
    movabsq    $4294967296, %rdx
    movq    %rax, %rbx
    movq    %rax, %rdi
    xorl    %esi, %esi
    call    memset
    movl    $42, 652(%rbx)
    movq    %rbx, %rdi
    popq    %rbx
    jmp    poke

and no 4GB temporary struct big is allocated on the stack. As this optimization can reduce the required stack size by arbitrary amounts, it has a significant semantic effect (similarly to tail call optimization).

However, RVO/DPS-translation in Rust is currently relatively fragile and occurs only in restricted circumstances. The in/box proposal has a higher-level interface to it, but lacks a low-level interface (pnkfelix’s draft implementation access it via a hack). It would be desirable to have a decent low-level interface.

The main reason for the fragility of RVO/DPS is that the destination must not be aliased during the expression’s evaluation - this makes it not work in the *p = φ case, as raw pointers have no aliasing information.

Detailed Design

Add the become-assignment expression.

Syntax

expr |= '*' 'become' expr '=' expr { ExprBecome(Expr, Expr) }

Typing

forall type T, expr DEST, expr EXPR.
DEST: *mut T, EXPR: T, T: Sized
------------------------------------
*become DEST = EXPR : ()

Semantics

become-assignment assigns the value of its expression to its destination without running destructors on the destination. The memory pointed-to by the destination is considered to be borrowed mutably during the evaluation of the expression (i.e. touching it will cause UB). become-assignment should be translate the expression in DPS style to the destination, and in any case must not allocate large temporaries.

Of course, become-assignment consumes the result of its expression, and is unsafe as it writes to a raw pointer.

Alternatives

We may want to introduce become-assignment for (safe) mutable references (in that case, the mutable borrow of the destination should of course be tracked by borrowck). In that case, we do have to run the destructors on the destination before the expression is evaluated, which would be inconsistent with the raw-pointer version. Also, it is unclear that this would be useful (users, this is your opportunity).

Drawbacks

Increased complexity.

Unresolved Questions

Syntax bikeshedding. Do we want to allow the &mut form?

There’s an RFC which has already been accepted, which solves the same problem, I think: https://github.com/rust-lang/rfcs/blob/master/text/0809-box-and-in-for-stdlib.md

"The in/box proposal has a higher-level interface to it, but lacks a low-level interface (pnkfelix’s draft implementation access it via a hack). "

Exactly.

The primary difference between this and = is that it does not run destructors, right?

Also, to clarify, the hack in question is having special code in trans for the “initialization intrinsic” (move_val_init (or whatever we call it these days)?

The difference is both that we don’t run destructors and that we have the noalias property (ptr assignment doesn’t AFAIK). This is indeed a replacement for move_val_init, which is an intrinsic that does not behave like a function.

FWIW, I think the &out (or &uninit, whatever) references which have been previously proposed would also provide the same functionality (in a safe way)?

If we could figure out a way to make them work with unwinding, then sure. I think we should probably have this feature be unstable until we decide what to do with these.

Yes, I see. While I am not a fan of magic intrinsics in general, I don’t see that this use case merits new syntax, at least not as presented. I’d like some evidence that it will be widely used. New syntax carries a very high price in terms of the perceived complexity of the language (accurate or not); much higher than a caveat on a random intrinsic you can find in the back of the language reference.

That’s why I wanted it to be perma-unstable syntax at first (maybe make it into HIR-only syntax?)

@eddyb discovered this can essentially be done by unsafe { ptr::write(&mut p, val) } (with optimizations)

I don’t really understand why a separate low-level interface is needed. Per the RFC, couldn’t you easily write a trivial Placer that just takes a raw pointer and returns it again from the pointer method? (That is, under the assumption that the actual in place { block } syntax will be directly implemented by the compiler and thus not need any special intrinsic or operator to do its job.)

1 Like

That’s a decent suggestion too (cc @pnkfelix).

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