Есть ли какие-либо негативные последствия для производительности при использовании локальных функций в Rust?

#function #memory #rust #runtime

#функция #память #Ржавчина #время выполнения

Вопрос:

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

 fn main() {
    fn local_plus(x: i64, y: i64) -> i64 {
        x   y
    }
    let x = 2i64;
    let y = 5i64;

    let local_res = local_plus(x, y);
    let external_res = external_plus(x,y);
    assert_eq!(local_res, external_res);
}

fn external_plus(x: i64, y: i64)  -> i64 {
    x   y
}
  

Мне было интересно, есть ли какие-либо негативные последствия для производительности при этом? Например, Rust повторно объявляет функцию или занимает нежелательный объем функционального пространства каждый раз, когда выполняется содержащая функция? Или это буквально не влияет на производительность?

В качестве отступления, любые советы о том, как я мог бы найти ответ для себя (либо прочитав какой-либо конкретный набор документов, либо инструменты, которые я мог бы использовать), будут приветствоваться.

Комментарии:

1. Вы могли бы найти это самостоятельно, запустив несколько тестов или проверив вывод MIR / assembly, как это сделал я.

2. @ljedrz: Бенчмарки — непостоянные звери, лучше всего проверить неоптимизированный и оптимизированный MIR / LLVM IR / assembly.

3. @MatthieuM. согласовано — тесты лучше подходят для более сложных случаев, когда другие способы уже непрактичны.

Ответ №1:

Влияния нет; Я проверил сборку, сгенерированную для обоих вариантов, и она идентична.

Две версии, которые я сравнил:

«внешний»:

 fn main() {
    let x = 2i64;
    let y = 5i64;

    let external_res = external_plus(x,y);
}

fn external_plus(x: i64, y: i64)  -> i64 {
    x   y
}
  

«локальный»:

 fn main() {
    fn local_plus(x: i64, y: i64) -> i64 {
        x   y
    }
    let x = 2i64;
    let y = 5i64;

    let local_res = local_plus(x, y);
}
  

И оба дают одинаковый результат asm (режим выпуска в сегодняшнем nightly):

     .text
    .file   "rust_out.cgu-0.rs"
    .section    .text._ZN8rust_out4main17hb497928495d48c40E,"ax",@progbits
    .p2align    4, 0x90
    .type   _ZN8rust_out4main17hb497928495d48c40E,@function
_ZN8rust_out4main17hb497928495d48c40E:
    .cfi_startproc
    retq
.Lfunc_end0:
    .size   _ZN8rust_out4main17hb497928495d48c40E, .Lfunc_end0-_ZN8rust_out4main17hb497928495d48c40E
    .cfi_endproc

    .section    .text.main,"ax",@progbits
    .globl  main
    .p2align    4, 0x90
    .type   main,@function
main:
    .cfi_startproc
    movq    %rsi, %rax
    movq    %rdi, %rcx
    leaq    _ZN8rust_out4main17hb497928495d48c40E(%rip), %rdi
    movq    %rcx, %rsi
    movq    %rax, %rdx
    jmp _ZN3std2rt10lang_start17h14cbded5fe3cd915E@PLT
.Lfunc_end1:
    .size   main, .Lfunc_end1-main
    .cfi_endproc


    .section    ".note.GNU-stack","",@progbits
  

Это означает, что в сгенерированном двоичном файле будет нулевая разница (не только с точки зрения производительности).

Более того, даже не имеет значения, используете ли вы функцию; следующий подход:

 fn main() {
    let x = 2i64;
    let y = 5i64;

    let res = x   y;
}
  

Также выдает ту же сборку.

Суть в том, что, как правило, функции встраиваются независимо от того, объявляете ли вы их в нем или вне его. main()

Редактировать: как указал Shepmaster, в этой программе нет побочных эффектов, поэтому сгенерированная сборка для обоих вариантов фактически такая же, как и для:

 fn main() {}
  

Однако вывод MIR для обоих тоже одинаков (и отличается от одного для пустого main() ), поэтому не должно быть никакой разницы в местоположении функции, даже если присутствовали побочные эффекты.

Комментарии:

1. Я почти уверен, что оптимизатор удаляет все в программе, поскольку у него нет заметных побочных эффектов. res полностью не используется, поэтому я думаю, вы увидите то же самое для fn main() {} .

2. @Shepmaster TIL :). Я добавлю это к ответу.

Ответ №2:

В качестве отступления, любые советы о том, как я мог бы найти ответ для себя (либо прочитав какой-либо конкретный набор документов, либо инструменты, которые я мог бы использовать), будут приветствоваться.

Знаете ли вы о игровой площадке Rust?

Введите свой код, нажмите «LLVM IR», «Assembly» или «MIR» вместо «Run», и вы увидите, какое низкоуровневое представление выдается для указанного кода.

Я лично предпочитаю LLVM IR (я привык читать его с C ), который все еще находится на более высоком уровне, чем assembly, хотя и остается языком post.

Мне было интересно, есть ли какие-либо негативные последствия для производительности при этом?

Это очень сложный вопрос; на самом деле.

Единственная разница между объявлением функции локально или извне в Rust заключается в области видимости. Объявление его локально просто уменьшает его область действия. Больше ничего.

Однако… область действия и использование могут иметь серьезные последствия для компиляции.

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

Итак, уменьшая область действия и тем самым ограничивая использование, вы поощряете компилятор рассматривать вашу функцию для встраивания (если вы не отметите ее «холодной»).

С другой стороны, поскольку область действия уменьшена, она не может быть разделена (очевидно).

Итак … что?

Следуйте инструкциям по использованию: определите элемент в максимально ограниченном объеме.

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

Имейте некоторое доверие к Rust, он не будет вводить накладные расходы, если сможет этого избежать.

Комментарии:

1. Отличный ответ! Хотя, возможно, вы хотите подчеркнуть, что прямых эффектов нет. Например, в Java есть некоторые виды «внутренних классов», которые автоматически ссылаются на внешний. То же самое с закрытиями Rust (проще говоря). Поэтому я бы сказал, что важно четко указать, что внутренние функции ни на что не ссылаются из своей заключающей функции и не занимают пространство стека заключающей функции. Только косвенные эффекты на встраивание.

2. @LukasKalbertodt: Я добавил здесь немного жирного. Я неохотно начинаю список того, на что это НЕ влияет, поскольку боюсь, что это может заставить людей подумать, что, возможно, если его нет в списке, это влияет, поэтому вместо этого я подчеркнул (подробнее), что это влияет только на область действия и ничего больше.