Почему этот код генерирует гораздо больше сборки, чем эквивалент С++/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)