I've heard somewhere that passing function arguments in registers is usually more preferable than passing them on stack.
I suppose for this reason System V ABI, a popular C ABI, introduces an elaborated system of breaking aggregate structures into parts and passing them in registers (see http://www.x86-64.org/documentation/abi.pdf , 3.2.3 Parameter Passing). I've also read somewhere that Rust uses some ABI very similar to System V.
For example, this program in C++ (see on gcc.godbolt.org - Compiler Explorer)
#include <cstdint>
// A structure analogous to Rust slice
struct Slice {
std::uint8_t* ptr;
std::uintptr_t len;
};
__attribute__((noinline)) void check_abi(Slice arg) {}
int main() {
auto a = Slice{(std::uint8_t*)12345, 54321};
check_abi(a);
}
compiles into the next code (fragment)
movq $12345, -16(%rbp)
movq $54321, -8(%rbp)
movq -16(%rbp), %rdx
movq -8(%rbp), %rax
movq %rdx, %rdi
movq %rax, %rsi
call check_abi(Slice)
i.e. the slice is broken into parts and passed in registers rdi
and rsi
in accordance with System V ABI.
Now let's look at the analogous program in Rust (see in Rust playpen - Rust Playground):
#[inline(never)]
fn check_abi(arg: &[u8]) {}
fn main() {
let a = b"abcdef";
check_abi(a);
}
In the produced LLVM IR I can see that the slice itself is stored on stack and the function accepts pointer to the slice:
%a = alloca { i8*, i64 }
%arg = alloca { i8*, i64 }
%0 = bitcast { i8*, i64 }* %a to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %0, i8* bitcast ({ i8*, i64 }* @const18 to i8*), i64 16, i32 8, i1 false)
%1 = bitcast { i8*, i64 }* %a to i8*
%2 = bitcast { i8*, i64 }* %arg to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %2, i8* %1, i64 16, i32 8, i1 false)
call void @_ZN9check_abi20h283fe1c03bf2ea21eaaE({ i8*, i64 }* noalias nocapture dereferenceable(16) %arg)
The assembler produced from LLVM IR behaves the same way (if I'm reading it correctly)
leaq 8(%rsp), %rdi
movq const18(%rip), %rax
movq %rax, 24(%rsp)
movq const18+8(%rip), %rax
movq %rax, 32(%rsp)
movq 24(%rsp), %rax
movq %rax, 8(%rsp)
movq 32(%rsp), %rax
movq %rax, 16(%rsp)
callq _ZN9check_abi20h283fe1c03bf2ea21eaaE
The slice is on stack - 8(%rsp)
and 16(%rsp)
and the pointer to the slice is passed in %rdi
according to the System V ABI.
I remember that this exact detail - effective passing in registers - was one of the main concerns when string_view, analogue of Rust's &str
, was adopted into C++.
So, the question is - why fat pointers, such pervasive types in Rust (&str
, &[T]
, &Trait
), are passed on stack and not in registers? Or am I wrong somewhere in my investigation?