Let’s say I want to write a function that takes a slice of ints and returns the sum of the first two values. I assume that the slice must contain two elements, otherwise I want to panic.
It looks like the simplest thing is the better for the compiler:
pub fn test(ns: &[i32]) -> i32 {
assert!(ns.len() >= 2);
return ns[0] + ns[1];
}
This produces the following:
example::test:
push rax
cmp rsi, 1
jbe .LBB7_1
mov eax, dword ptr [rdi + 4]
add eax, dword ptr [rdi]
pop rcx
ret
.LBB7_1:
lea rdi, [rip + .Lbyte_str.9]
lea rdx, [rip + .Lbyte_str.8]
mov esi, 31
call std::panicking::begin_panic
ud2
Exactly what I want: one check and one branch.
Now, something slightly different:
pub fn test(ns: &[i32]) -> i32 {
return ns[0] + ns[1];
}
I would expect that I have two checks for the size, the slice is const and the two checks can be collapsed (because sz > 0 && sz > 1 == sz > 1). Let’s see…
example::test:
push rax
test rsi, rsi
je .LBB6_3
cmp rsi, 1
je .LBB6_4
mov eax, dword ptr [rdi + 4]
add eax, dword ptr [rdi]
pop rcx
ret
.LBB6_3:
lea rdi, [rip + .Lpanic_bounds_check_loc.5]
xor esi, esi
xor edx, edx
call core::panicking::panic_bounds_check@PLT
ud2
.LBB6_4:
lea rdi, [rip + .Lpanic_bounds_check_loc.6]
mov esi, 1
mov edx, 1
call core::panicking::panic_bounds_check@PLT
ud2
Uuuhhh… Ok, I understand what is going on. The two panics are different, because they refer to distinct problems. But is what the writer of the function is expecting? I would just think “ok, I want to panic if there are less than 2 elements, indexing already does that for me. Why bothering with assert!
?”. Honestly, I don’t know if there is something that could be done here, but it was worth to mention the situation.
Let’s go on. We don’t like to use panic
ing indexing. But for now we still want to panic instead of returning an error, for reasons. We have beautiful zero-overhead abstractions, right?
pub fn test(ns: &[i32]) -> i32 {
match ns.get(..2usize) {
Some(&[a, b]) => a + b,
_ => unreachable!(),
}
}
We use slice pattern matching to get the two first values in case they exist, and for now we can just use unreachable!()
to panic. Let’s look at the assembly:
example::test:
push rax
xor ecx, ecx
cmp rsi, 2
cmovae rcx, rdi
cmp rsi, 1
jbe .LBB8_1
mov eax, dword ptr [rcx + 4]
add eax, dword ptr [rcx]
pop rcx
ret
.LBB8_1:
lea rdi, [rip + .Lbyte_str.d]
lea rdx, [rip + .Lbyte_str.c]
mov esi, 40
call std::panicking::begin_panic
ud2
Maybe some of you which are asm experts do expect this, but honestly I don’t get it. Why there are 2 comparisons against rsi
? rsi
contains the size of the slice, and in case it is above or equal (greater or equal for unsigned values), rdi
is moved to rcx
. Now the size is checked again; if it is less or equal to 1 we jump to the panic trampoline (how it is called that part? ), otherwise we get the two values, sum them together in eax
and we return the result. My point is that rcx
is used only in case we have at least 2 values, and rdi
could be used directly. To, me, it looks totally useless to check two times the value of rsi
, conditionally moving rdi
to `rcx’ and then using this register.
Am I missing something?