Вопрос оптимизации: как лучше возвращать структуры из функции, что бы не было лишних выделений и копирований областей памяти? Когда пишешь код, то кажется, что если экземпляр структуры размещается на стеке, то он должен копироваться в стек вызывающей функции и оптимальнее возвращать указатель. Но по анализу ассемблерного кода, сделанного компилятором, получается, что все не совсем так.
Простая программа с 3 вариантами (возврат значения, указателя и Option):
struct MyStruct { value1: i64, value2: i64, value3: i64, } fn create_struct_opt() -> Option<MyStruct> { 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<MyStruct> { 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
С Option действий побольше. Выделяем место в стеке размером со структуру, записываем туда наши значения, а затем копируем значения из «локального» стека функции по переданному адресу. (зачем? в релизной сборке нет этих лишних копирований):
; enum2<core::option::Option<rustplay::MyStruct> > *__fastcall rustplay::create_struct_opt(enum2<core::option::Option<rustplay::MyStruct> > *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
Выводы
Для простых структур оптимальнее делать возврат «по значению», даже если компилятор не за инлайнит функцию.
Не нужно делать преждевременную оптимизацию за компилятор, он сам сделает оптимальный вариант. На этапе написания кода сложно угадать правильный способ, нужно смотреть дизассемблированный код релизной сборки и только после этого делать рефакторинг.
