Konfusion about ownership on calculations

Hello everyone,
I'm new to Rust and started to play around a bit to write simple things to learn about ownership and borrowing. In the moment i thought: "Ok now you have the basics!", i come to the point of some simple calculations and things don't act as i think they have to ...

Code snippet with assembly can be found here: godbolt.org

pub fn f1(){
    let a1: i32 = 11;
    let b1: i32 = 22;
    
    let c1: i32 = a1 + b1;
    let d1: i32 = a1;
}

pub fn f2(){
    let a2: i32 = 11;
    let b2: i32 = 22;
    
    let c2: i32 = &a2 + &b2;
    let d2: i32 = a2;
}

I dont understand why the ownership of a1 and b1 in f1 is not going over to c1?
For me it looks like both function return the same value so they are "functional identical" but in assembly f1 uses more instructions then f2.
What way is the preferred way in Rust or better in the community?

Greetings Marc

i32 and other primitive scalar types implement Copy, which means ownership is not transferred by assignment. Instead, a fresh bit-wise copy of the value is made.

Are you compiling with --release? Please show the asm.

1 Like

Oh that was fast thank you!
Yes i looked at it last weekend in the rust book and forgot it directly. Thanks for taking time i will read this chapter again!

For the assembly i just used the website godbolt.org:
Compiler Explorer

Not sure how good the assembly looks here.
Compiler: rustc 1.84.0

<&i32 as core::ops::arith::Add<&i32>>::add::hf4e8167fde4f863c:
        sub     rsp, 40
        mov     qword ptr [rsp], rdx
        mov     qword ptr [rsp + 16], rdi
        mov     qword ptr [rsp + 24], rsi
        mov     eax, dword ptr [rdi]
        mov     dword ptr [rsp + 32], eax
        mov     ecx, dword ptr [rsi]
        mov     dword ptr [rsp + 36], ecx
        add     eax, ecx
        mov     dword ptr [rsp + 12], eax
        seto    al
        jo      .LBB0_2
        mov     eax, dword ptr [rsp + 12]
        add     rsp, 40
        ret
.LBB0_2:
        mov     rdi, qword ptr [rsp]
        mov     rax, qword ptr [rip + core::panicking::panic_const::panic_const_add_overflow::h5a5f82b06563d133@GOTPCREL]
        call    rax

f1:
        sub     rsp, 24
        mov     dword ptr [rsp + 12], 22
        mov     dword ptr [rsp + 16], 11
        mov     eax, 11
        add     eax, 22
        mov     dword ptr [rsp + 8], eax
        seto    al
        jo      .LBB1_2
        mov     eax, dword ptr [rsp + 8]
        mov     dword ptr [rsp + 20], eax
        add     rsp, 24
        ret
.LBB1_2:
        lea     rdi, [rip + .L__unnamed_1]
        mov     rax, qword ptr [rip + core::panicking::panic_const::panic_const_add_overflow::h5a5f82b06563d133@GOTPCREL]
        call    rax

f2:
        sub     rsp, 24
        mov     dword ptr [rsp + 12], 11
        mov     dword ptr [rsp + 16], 22
        lea     rdi, [rsp + 12]
        lea     rsi, [rsp + 16]
        lea     rdx, [rip + .L__unnamed_2]
        call    <&i32 as core::ops::arith::Add<&i32>>::add::hf4e8167fde4f863c
        mov     dword ptr [rsp + 20], eax
        add     rsp, 24
        ret

.L__unnamed_3:
        .ascii  "/app/example.rs"

.L__unnamed_1:
        .quad   .L__unnamed_3
        .asciz  "\017\000\000\000\000\000\000\000\016\000\000\000\023\000\000"

.L__unnamed_2:
        .quad   .L__unnamed_3
        .asciz  "\017\000\000\000\000\000\000\000\027\000\000\000\023\000\000"

some mention about which way is the better way to write clean code?

You didn’t specify any options, so this is unoptimized output, which should never be used to judge anything; the only thing it is good for is predictable debugging and being fast to compiler. If you write -C opt-level=3 in the compiler flags box, the compiler will make the two functions identical — by deleting all of the instructions, because they don't return or do anything observable outside. A proper test should involve functions that have parameters and return values, like this:

#[no_mangle]
pub fn f1(a1: i32, b1: i32) -> (i32, i32) {
    let c1: i32 = a1 + b1;
    let d1: i32 = a1;
    (c1, d1)
}

#[no_mangle]
pub fn f2(a2: i32, b2: i32) -> (i32, i32) {
    let c2: i32 = &a2 + &b2;
    let d2: i32 = a2;
    (c2, d2)
}

In this specific instance, I'd generally prefer the first example (a1 + b1, speciflcally). Borrowing the operands to + in this expression doesn't do anything, because the types involved are Copy anyways, and I'm a big fan of removing even small things when they're useless.

Borrow when you need to borrow. The compiler will tell you a value has been moved when you're trying to use it afterwards, which is a strong hint that you need to look at how the value is being used and make a decision about borrowing, cloning, or otherwise finding a way to use it twice.

1 Like

Yes that's a good hint thank you! I think i first learn rust better before throw around some assembly next time :slight_smile:

Sounds like a good start point to not over complicate things in the beginning!

Thanks guys for the fast help!

I suggest using String for those experimentations: A type that's not Copy and needs to do something when dropped.

Otherwise you'll just see a bunch of convenience features where you're allowed to do more with simple things like i32 that don't need the same restrictions.

3 Likes