Вопрос оптимизации: как лучше возвращать структуры из функции, что бы не было лишних выделений и копирований областей памяти? Когда пишешь код, то кажется, что если экземпляр структуры размещается на стеке, то он должен копироваться в стек вызывающей функции и оптимальнее возвращать указатель. Но по анализу ассемблерного кода, сделанного компилятором, получается, что все не совсем так.
Простая программа с 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Выводы
Для простых структур оптимальнее делать возврат «по значению», даже если компилятор не за инлайнит функцию.
Не нужно делать преждевременную оптимизацию за компилятор, он сам сделает оптимальный вариант. На этапе написания кода сложно угадать правильный способ, нужно смотреть дизассемблированный код релизной сборки и только после этого делать рефакторинг.
 
           
 
viktordev
Это написано в любой книжке по расту.
ogregor
Все равно интересно