Почему этот код генерирует гораздо больше сборки, чем эквивалент С++/Clang?
Я написал простую функцию С++ для проверки оптимизации компилятора:
bool f1(bool a, bool b) {
return !a || (a && b);
}
После этого я проверил эквивалент в Rust:
fn f1(a: bool, b: bool) -> bool {
!a || (a && b)
}
Я использовал godbolt, чтобы проверить выход ассемблера.
Результат кода С++ (скомпилированный clang с флагом -O3) выглядит следующим образом:
f1(bool, bool): # @f1(bool, bool)
xor dil, 1
or dil, sil
mov eax, edi
ret
И результат эквивалента Rust намного длиннее:
example::f1:
push rbp
mov rbp, rsp
mov al, sil
mov cl, dil
mov dl, cl
xor dl, -1
test dl, 1
mov byte ptr [rbp - 3], al
mov byte ptr [rbp - 4], cl
jne .LBB0_1
jmp .LBB0_3
.LBB0_1:
mov byte ptr [rbp - 2], 1
jmp .LBB0_4
.LBB0_2:
mov byte ptr [rbp - 2], 0
jmp .LBB0_4
.LBB0_3:
mov al, byte ptr [rbp - 4]
test al, 1
jne .LBB0_7
jmp .LBB0_6
.LBB0_4:
mov al, byte ptr [rbp - 2]
and al, 1
movzx eax, al
pop rbp
ret
.LBB0_5:
mov byte ptr [rbp - 1], 1
jmp .LBB0_8
.LBB0_6:
mov byte ptr [rbp - 1], 0
jmp .LBB0_8
.LBB0_7:
mov al, byte ptr [rbp - 3]
test al, 1
jne .LBB0_5
jmp .LBB0_6
.LBB0_8:
test byte ptr [rbp - 1], 1
jne .LBB0_1
jmp .LBB0_2
Я также пробовал с опцией -O
, но вывод пуст (удаленная неиспользуемая функция).
Я намеренно НЕ использую какую-либо библиотеку, чтобы поддерживать чистоту вывода. Обратите внимание, что как clang
, так и rustc
используют LLVM в качестве бэкэнд. Что объясняет эту огромную разницу в производительности? И если это проблема только с отключенным оптимизатором, как я могу увидеть оптимизированный вывод из rustc
?
Ответы
Ответ 1
Компиляция с флагом компилятора -O
(и с добавленным pub
), я получаю этот вывод (Ссылка на Godbolt):
push rbp
mov rbp, rsp
xor dil, 1
or dil, sil
mov eax, edi
pop rbp
ret
Несколько вещей:
-
Почему он еще длиннее версии С++?
Версия Rust состоит из трех инструкций:
push rbp
mov rbp, rsp
[...]
pop rbp
Это инструкции для управления так называемым указателем фрейма или базовым указателем (rbp
). Это в основном требуется, чтобы получить хорошие трассировки стека. Если вы отключите его для версии С++ с помощью -fno-omit-frame-pointer
, вы получите тот же результат. Обратите внимание, что это использует g++
вместо clang++
, так как я не нашел сопоставимую опцию для компилятора clang.
-
Почему Rust не пропускает указатель кадра?
Собственно, так оно и есть. Но Godbolt добавляет возможность компилятору сохранить указатель кадра. Вы можете узнать больше о том, почему это сделано здесь. Если вы компилируете свой код локально с помощью rustc -O --crate-type=lib foo.rs --emit asm -C "llvm-args=-x86-asm-syntax=intel"
, вы получите этот вывод:
f1:
xor dil, 1
or dil, sil
mov eax, edi
ret
Это именно вывод вашей версии на С++.
Вы можете "отменить" то, что делает Godbolt, передать -C debuginfo=0
компилятору.
-
Почему -O
вместо --release
?
Godbolt использует rustc
непосредственно вместо cargo
. Флаг --release
является флагом для cargo
. Чтобы включить оптимизацию на rustc
, вам необходимо пройти -O
или -C opt-level=3
(или любой другой уровень между 0 и 3).
Ответ 2
Компиляция с -C opt-level=3
в godbolt дает:
example::f1:
push rbp
mov rbp, rsp
xor dil, 1
or dil, sil
mov eax, edi
pop rbp
ret
Что выглядит сравнимо с версией на С++. См. ответ Лукаса Кальбердотта для получения более подробного объяснения.
Примечание. Мне пришлось сделать функцию pub extern
, чтобы остановить компилятор, оптимизируя его до нуля, так как он не используется.
Ответ 3
Чтобы получить тот же код asm, вам нужно отключить информацию об отладке - это приведет к удалению указателей фреймов.
-C opt-level=3 -C debuginfo=0
(https://godbolt.org/g/vdhB2f)
Ответ 4
Это не (фактическая разница намного меньше, чем показано в вопросе). Я удивлен, что никто не проверял вывод С++:
godbolt С++ x64 clang 4.0, без параметров компилятора
godbolt Rust 1.18, нет параметров компилятора