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

5f868cfd8e828df18990667509d1aa2e

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

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

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

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

© Habrahabr.ru