Выбор варианта возврата значения из функций в Rust

от автора

Вопрос оптимизации: как лучше возвращать структуры из функции, что бы не было лишних выделений и копирований областей памяти? Когда пишешь код, то кажется, что если экземпляр структуры размещается на стеке, то он должен копироваться в стек вызывающей функции и оптимальнее возвращать указатель. Но по анализу ассемблерного кода, сделанного компилятором, получается, что все не совсем так.

Простая программа с 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

Выводы

Для простых структур оптимальнее делать возврат «по значению», даже если компилятор не за инлайнит функцию.

Не нужно делать преждевременную оптимизацию за компилятор, он сам сделает оптимальный вариант. На этапе написания кода сложно угадать правильный способ, нужно смотреть дизассемблированный код релизной сборки и только после этого делать рефакторинг.


ссылка на оригинал статьи https://habr.com/ru/articles/778960/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *