Выбор варианта возврата значения из функций в Rust
Вопрос оптимизации: как лучше возвращать структуры из функции, что бы не было лишних выделений и копирований областей памяти? Когда пишешь код, то кажется, что если экземпляр структуры размещается на стеке, то он должен копироваться в стек вызывающей функции и оптимальнее возвращать указатель. Но по анализу ассемблерного кода, сделанного компилятором, получается, что все не совсем так.
Простая программа с 3 вариантами (возврат значения, указателя и Option):
struct MyStruct {
value1: i64,
value2: i64,
value3: i64,
}
fn create_struct_opt() -> Option {
let my_struct = MyStruct {
value1: 1,
value2: 2,
value3: 3,
};
return Some(my_struct);
}
fn create_struct() -> MyStruct {
let my_struct = MyStruct {
value1: 1,
value2: 2,
value3: 3,
};
return my_struct;
}
fn create_struct_box() -> Box {
let my_struct = Box::new(MyStruct {
value1: 1,
value2: 2,
value3: 3,
});
return my_struct;
}
fn main() {
let my_struct = create_struct();
let my_struct_box = create_struct_box();
let my_struct_ref = create_struct_opt();
println!(
"Значение my_struct: {} {} {}",
my_struct_ref.unwrap().value1,
my_struct.value1,
my_struct_box.value1
);
}
Компилирую в Windows используя toolchain stable-x86_64-pc-windows-msvc. Для других компиляторов код может быть другой
Собираем debug сборку и смотрим дизассемблером вызов интересующих нас функций в функции main:
lea rcx, [rbp+120h+result] ; result
call rustplay__create_struct
call rustplay__create_struct_box
mov [rbp+120h+var_130], rax
lea rcx, [rbp+120h+var_128]
call rustplay__create_struct_opt
Псевдокод:
rustplay::create_struct((rustplay::MyStruct *)&v0[11]);
struct_box = rustplay::create_struct_box();
rustplay::create_struct_opt(&v8);
Тут уже видно, что в функции передаются адреса на стеке вместо возврата адресов из функций.
В первом случае функция main выделят память на стеке размером со структуру и передает адрес в функцию create_struct. Эта функция просто напрямую пишет значения по переданному адресу:
; rustplay::MyStruct *__fastcall rustplay::create_struct(rustplay::MyStruct *result)
rustplay__create_struct proc near
mov rax, rcx
mov qword ptr [rcx], 1
mov qword ptr [rcx+8], 2
mov qword ptr [rcx+10h], 3
retn
rustplay__create_struct endp
С Options действий побольше. Выделяем стек размером со структуру, записываем туда наши значения, а затем копируем значения из «локального» стека функции в переданный адрес. (зачем? в релизной сборке нет этих лишних копирований):
; enum2 > *__fastcall rustplay::create_struct_opt(enum2 > *result)
rustplay__create_struct_opt proc near
var_18= qword ptr -18h
var_10= qword ptr -10h
var_8= qword ptr -8
sub rsp, 18h
mov rax, rcx
mov [rsp+18h+var_18], 1
mov [rsp+18h+var_10], 2
mov [rsp+18h+var_8], 3
mov rdx, [rsp+18h+var_18]
mov [rcx+8], rdx
mov rdx, [rsp+18h+var_10]
mov [rcx+10h], rdx
mov rdx, [rsp+18h+var_8]
mov [rcx+18h], rdx
mov qword ptr [rcx], 1
add rsp, 18h
retn
rustplay__create_struct_opt endp
Ну и третий вариант с указателем на кучу (Box), самый медленный.
push rbp
; инициализируем локальный стек
sub rsp, 50h
lea rbp, [rsp+50h]
mov [rbp+var_8], 0FFFFFFFFFFFFFFFEh
; записываем значения структуры в локальный стек функции
mov [rbp+var_28], 1
mov [rbp+var_20], 2
mov [rbp+var_18], 3
; выделяем память в куче
loc_14000194A: ; unsigned __int64
; try {
mov ecx, 18h
mov edx, 8 ; unsigned __int64
call _ZN5alloc5alloc15exchange_malloc17hb41a2de4aa8d1871E ; alloc::alloc::exchange_malloc::hb41a2de4aa8d1871
; } // starts at 14000194A
loc_140001959:
mov [rbp+var_30], rax
loc_14000195F:
; копируем значения со стека в кучу
mov rax, [rbp+var_30]
mov rcx, [rbp+var_28]
mov [rax], rcx
mov rcx, [rbp+var_20]
mov [rax+8], rcx
mov rcx, [rbp+var_18]
mov [rax+10h], rcx
mov [rbp+var_10], rax
add rsp, 50h
pop rbp
; в rax адрес в куче
retn
;и затем еще и очистка
loc_140001990:
; cleanup() // owned by 14000194A
mov [rsp-8+arg_8], rdx
push rbp
sub rsp, 20h
lea rbp, [rdx+50h]
add rsp, 20h
pop rbp
retn
В release сборке компилятор вообще делает все inline. При этом возврат указателя остается самым медленным, т.к. идет выделение куска памяти в куче.
push rbp
sub rsp, 0C0h
lea rbp, [rsp+80h]
mov [rbp+40h+var_8], 0FFFFFFFFFFFFFFFEh
; возврат структуры напрямую
mov [rbp+40h+var_28], 1
mov [rbp+40h+var_20], 2
mov [rbp+40h+var_18], 3
; Box
movzx eax, cs:__rust_no_alloc_shim_is_unstable
mov ecx, 18h
mov edx, 8
call __rust_alloc
test rax, rax
mov qword ptr [rax], 1
mov qword ptr [rax+8], 2
mov qword ptr [rax+10h], 3
; Option
mov [rbp+40h+var_40], 1
mov [rbp+40h+var_38], 2
mov [rbp+40h+var_30], 3
Выводы
Для простых структур оптимальнее делать возврат «по значению», даже если компилятор не за инлайнит функцию.
Не нужно делать преждевременную оптимизацию за компилятор, он сам сделает оптимальный вариант. На этапе написания кода сложно угадать правильный способ, нужно смотреть дизассемблированный код релизной сборки и только после этого делать рефакторинг.