What is &2? Or &999? Why?

I am quite curious about something I saw in the Results code. In the following, what is "(&2)"? Why "Ok(2)" but "contans(&2)"?

Is the digit 2 stored at that address? Whatever it is... Why? It's clearly an optimization... maybe...

    /// #![feature(option_result_contains)]
    ///
    /// let x: Result<u32, &str> = Ok(2);
    /// assert_eq!(x.contains(&2), true);
    ///
    /// let x: Result<u32, &str> = Ok(3);
    /// assert_eq!(x.contains(&2), false);
    ///
    /// let x: Result<u32, &str> = Err("Some error message");
    /// assert_eq!(x.contains(&2), false);

Link:

This is indeed an efficiency thing, where taking a value by reference or by value doesn't make a difference on the output, it's possible that the value being tested is non-Copy or is too expensive to Clone, so therefore a reference works well in this case.

Technically they could make it be the following:

pub fn contains<U>(&self, x: &U) -> bool where U: PartialEq<T> {
    match self {
        Ok(y) => x == y,
        Err(_) => false
    }
}
pub fn contains_copy<U>(&self, x: U) -> bool where U: Copy + PartialEq<T> {
    match self {
        Ok(y) => x == y,
        Err(_) => false
    }
}

But that already takes a reference to the argument as it expands to the following:

pub fn contains<U>(&self, x: &U) -> bool where U: PartialEq<T> {
    match self {
        Ok(y) => std::cmp::PartialEq::eq(x, y),
        Err(_) => false
    }
}
pub fn contains_copy<U>(&self, x: U) -> bool where U: Copy + PartialEq<T> {
    match self {
        Ok(y) => std::cmp::PartialEq::eq(x, &y),
        Err(_) => false
    }
}

because PartialEq::eq is defined like so:

  fn eq(&self, other: &Rhs) -> bool;
2 Likes

Honestly I wouldn't even say it's an optimization. Alright, a Copy or a Clone may be too expensive for some types, but what about types that aren't Clone and thus are impossible to replicate? In that case testing for containment would move the value and the caller would lose it whether or not s/he still needs it. Thus, to me this is primarily an accessible/conscious API design thing, not really an optimization.

Is the digit 2 stored at that address?

Formally, this results in a temporary of the integer value 2 (of type u32) with an appropriate lifetime, to which a pointer is then taken immediately. If I recall correctly, the type system promotes these trivial constant values to 'static lifetime as though they were baked in the executable similar to string literals.

However, I find it unlikely that this is actually what happens in the generated code; the optimizer probably pulls out the immediate value to an instruction or a register after having inlined the contains() function, then performs the comparison directly without any extra indirection.

5 Likes

Let's look at the MIR of the following code:

fn contains<T> (_: &T) {}

fn bar ()
{
    static TWO: i32 = 2;

    contains(&TWO);
}
static  TWO: i32 = {
    let mut _0: i32;                     // return place in scope 0 at src/lib.rs:3:13: 3:16

    bb0: {
        _0 = const 2i32;                 // bb0[0]: scope 0 at src/lib.rs:3:19: 3:20
                                         // ty::Const
                                         // + ty: i32
                                         // + val: Scalar(Bits { size: 4, bits: 2 })
                                         // mir::Constant
                                         // + span: src/lib.rs:3:19: 3:20
                                         // + ty: i32
                                         // + literal: Const { ty: i32, val: Scalar(Bits { size: 4, bits: 2 }) }
        return;                          // bb0[1]: scope 0 at src/lib.rs:3:1: 3:21
    }
}

fn  contains(_1: &T) -> () {
    let mut _0: ();                      // return place in scope 0 at src/lib.rs:1:24: 1:24

    bb0: {
        return;                          // bb0[0]: scope 0 at src/lib.rs:1:26: 1:26
    }
}

fn  bar() -> () {
    let mut _0: ();                      // return place in scope 0 at src/lib.rs:11:1: 11:1
    let mut _1: ();                      // in scope 0 at src/lib.rs:12:5: 12:19
    let mut _2: &i32;                    // in scope 0 at src/lib.rs:12:14: 12:18
    let mut _3: &i32;                    // in scope 0 at src/lib.rs:12:14: 12:18

    bb0: {
        StorageLive(_2);                 // bb0[0]: scope 0 at src/lib.rs:12:14: 12:18
        StorageLive(_3);                 // bb0[1]: scope 0 at src/lib.rs:12:14: 12:18
        _3 = &(TWO: i32);                // bb0[2]: scope 0 at src/lib.rs:12:14: 12:18
        _2 = _3;                         // bb0[3]: scope 0 at src/lib.rs:12:14: 12:18
        _1 = const contains(move _2) -> bb1; // bb0[4]: scope 0 at src/lib.rs:12:5: 12:19
                                         // ty::Const
                                         // + ty: for<'r> fn(&'r i32) {contains::<i32>}
                                         // + val: Scalar(Bits { size: 0, bits: 0 })
                                         // mir::Constant
                                         // + span: src/lib.rs:12:5: 12:13
                                         // + ty: for<'r> fn(&'r i32) {contains::<i32>}
                                         // + literal: Const { ty: for<'r> fn(&'r i32) {contains::<i32>}, val: Scalar(Bits { size: 0, bits: 0 }) }
    }

    bb1: {
        StorageDead(_2);                 // bb1[0]: scope 0 at src/lib.rs:12:18: 12:19
        StorageDead(_3);                 // bb1[1]: scope 0 at src/lib.rs:12:19: 12:20
        return;                          // bb1[2]: scope 0 at src/lib.rs:13:2: 13:2
    }
}

vs.


fn contains<T> (_: &T) {}

fn foo ()
{
    contains(&2);
}
fn  foo() -> () {
    let mut _0: ();                      // return place in scope 0 at src/lib.rs:4:1: 4:1
    let mut _1: ();                      // in scope 0 at src/lib.rs:5:5: 5:17
    let mut _2: &i32;                    // in scope 0 at src/lib.rs:5:14: 5:16
    let mut _3: &i32;                    // in scope 0 at src/lib.rs:5:14: 5:16

    bb0: {
        StorageLive(_2);                 // bb0[0]: scope 0 at src/lib.rs:5:14: 5:16
        StorageLive(_3);                 // bb0[1]: scope 0 at src/lib.rs:5:14: 5:16
        _3 = &(promoted[0]: i32);        // bb0[2]: scope 0 at src/lib.rs:5:14: 5:16
        _2 = _3;                         // bb0[3]: scope 0 at src/lib.rs:5:14: 5:16
        _1 = const contains(move _2) -> bb1; // bb0[4]: scope 0 at src/lib.rs:5:5: 5:17
                                         // ty::Const
                                         // + ty: for<'r> fn(&'r i32) {contains::<i32>}
                                         // + val: Scalar(Bits { size: 0, bits: 0 })
                                         // mir::Constant
                                         // + span: src/lib.rs:5:5: 5:13
                                         // + ty: for<'r> fn(&'r i32) {contains::<i32>}
                                         // + literal: Const { ty: for<'r> fn(&'r i32) {contains::<i32>}, val: Scalar(Bits { size: 0, bits: 0 }) }
    }

    bb1: {
        StorageDead(_2);                 // bb1[0]: scope 0 at src/lib.rs:5:16: 5:17
        StorageDead(_3);                 // bb1[1]: scope 0 at src/lib.rs:5:17: 5:18
        return;                          // bb1[2]: scope 0 at src/lib.rs:6:2: 6:2
    }
}

promoted[0] in  foo: i32 = {
    let mut _0: i32;                     // return place in scope 0 at src/lib.rs:5:14: 5:16
    let mut _1: i32;                     // in scope 0 at src/lib.rs:5:15: 5:16

    bb0: {
        _1 = const 2i32;                 // bb0[0]: scope 0 at src/lib.rs:5:15: 5:16
                                         // ty::Const
                                         // + ty: i32
                                         // + val: Scalar(Bits { size: 4, bits: 2 })
                                         // mir::Constant
                                         // + span: src/lib.rs:5:15: 5:16
                                         // + ty: i32
                                         // + literal: Const { ty: i32, val: Scalar(Bits { size: 4, bits: 2 }) }
        _0 = move _1;                    // bb0[1]: scope 0 at src/lib.rs:5:14: 5:16
        return;                          // bb0[2]: scope 0 at src/lib.rs:5:14: 5:16
    }
}

fn  contains(_1: &T) -> () {
    let mut _0: ();                      // return place in scope 0 at src/lib.rs:1:24: 1:24

    bb0: {
        return;                          // bb0[0]: scope 0 at src/lib.rs:1:26: 1:26
    }
}

Since 2 is a literal, it is a const expression, and when requiring the address of a const expression*, the const expression is promoted to static, so that its value does not need to be copied in the stack and its address is thus a known constant itself.

* when taking an immutable reference; when otherwise doing &mut CONST_EXPR, the const expression is copied into a new stack temporary, so that the programmer can effectivel get a unique reference to it.
3 Likes