Fat pointers and calling conventions

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?

1 Like

This issue I filed a couple weeks ago looks relevant: Seemingly inefficient code generated to forward a parameter to a function · Issue #22891 · rust-lang/rust · GitHub.

Btw, I've looked at what Clang does and it splits the structures into parts already at the LLVM IR level:

call void @check_abi(Slice)(i8* %7, i64 %9), !dbg !95