Сложный вывод компилятора для простого конструктора - PullRequest
3 голосов
/ 25 марта 2019

У меня есть структура X с двумя 64-битными целочисленными членами и конструктор:

struct X
{
    X(uint64_t a, uint64_t b)
    {
        a_ = a; b_ = b;
    }

    uint64_t a_, b_;
};

Когда я смотрю на вывод компилятора (x86-64 gcc 8.3 и x86-64 clang 8.0.0, на 64-битной Linux), без включенной оптимизации, я вижу следующий код для конструктора.

x86-64 gcc 8,3:

X::X(unsigned long, unsigned long):
    push    rbp
    mov     rbp, rsp
    mov     QWORD PTR [rbp-8], rdi
    mov     QWORD PTR [rbp-16], rsi
    mov     QWORD PTR [rbp-24], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     QWORD PTR [rax], 0
    mov     rax, QWORD PTR [rbp-8]
    mov     QWORD PTR [rax+8], 0
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-16]
    mov     QWORD PTR [rax+8], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-24]
    mov     QWORD PTR [rax], rdx
    nop
    pop     rbp
    ret

x86-64 clang 8.0.0:

X::X(unsigned long, unsigned long):
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     qword ptr [rbp - 16], rsi
    mov     qword ptr [rbp - 24], rdx
    mov     rdx, qword ptr [rbp - 8]
    mov     qword ptr [rdx], 0
    mov     qword ptr [rdx + 8], 0
    mov     rsi, qword ptr [rbp - 16]
    mov     qword ptr [rdx + 8], rsi
    mov     rsi, qword ptr [rbp - 24]
    mov     qword ptr [rdx], rsi
    pop     rbp
    ret

Кто-нибудь знает, почему вывод такой сложный? Я ожидал бы двух простых операторов "mov" даже без включенной оптимизации.

Ответы [ 3 ]

7 голосов
/ 25 марта 2019

Неоптимизированный код всегда сохраняет все переменные C ++ (включая аргументы функций) в их ячейке памяти между операторами, , чтобы значения были доступны для чтения отладчиком и даже , изменения , (И поскольку он не тратил никакого времени на распределение регистров.) Это включает в себя сохранение регистровых аргументов в памяти до первого оператора C ++ функции.


Это синтаксическая сборка Intel, подобная gcc -masm=intel, поэтому она использует пункт назначения, исходный порядок. (Мы можем определить это, используя PTR, квадратные скобки и отсутствие % в именах регистров.)

Первые 3 хранилища - это аргументы функции (this, a, b), которые были переданы в регистрах RDI, RSI и RDX в соответствии с соглашением о вызовах ABI системы V системы x86-64.

mov     QWORD PTR [rbp-8], rdi        # this
mov     QWORD PTR [rbp-16], rsi       # a
mov     QWORD PTR [rbp-24], rdx       # b

Теперь он загружает this в rax и записывает нули в a_ и b_, потому что вы не использовали правильную инициализацию конструктора. Или, возможно, вы добавили инициализацию к нулю с помощью некоторого кода, который вы здесь не показывали, или нечетного параметра компилятора.

mov     rax, QWORD PTR [rbp-8]
mov     QWORD PTR [rax], 0           # this->a_ = 0
mov     rax, QWORD PTR [rbp-8]
mov     QWORD PTR [rax+8], 0         # this->b_ = 0

Затем он загружает this в rax снова и a в rdx, затем записывает this->a_ с rdx aka a. То же самое для b.

Подождите, на самом деле это должна быть сначала запись в b_, а затем запись в a_, потому что структуры должны соответствовать объявлению и порядку памяти. Так что [rax+8] должно быть b_, а не a_.

mov     rax, QWORD PTR [rbp-8]
mov     rdx, QWORD PTR [rbp-16]        # reload a
mov     QWORD PTR [rax+8], rdx         # this->b_ = a
mov     rax, QWORD PTR [rbp-8]
mov     rdx, QWORD PTR [rbp-24]        # reload b
mov     QWORD PTR [rax], rdx           # this->a_ = b

Таким образом, ваш asm не соответствует источнику C ++ в вашем вопросе.

3 голосов
/ 25 марта 2019

Что происходит и почему?

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

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

Код с -O0:

X::X(unsigned long, unsigned long):
    push    rbp        // Push the frame pointer to the stack
    mov     rbp, rsp   // Copy the frame pointer to the rsb register
    // Create the object (on the stack)
    mov     QWORD PTR [rbp-8], rdi  
    mov     QWORD PTR [rbp-16], rsi
    mov     QWORD PTR [rbp-24], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-16]
    mov     QWORD PTR [rax], rdx
    mov     rax, QWORD PTR [rbp-8]
    mov     rdx, QWORD PTR [rbp-24]
    mov     QWORD PTR [rax+8], rdx
    nop     // IDEK why it does this
    // Pop the frame pointer
    pop     rbp
    ret

Код с -O1:

X::X(unsigned long, unsigned long):
    mov     rax, rdi
    mov     rdx, rsi
    ret

Имеет ли это значение?

Вид. Код без оптимизации намного медленнее, особенно потому, что компилятор должен делать такие вещи. Но в большинстве случаев , а не нет причин для оптимизации.

Как отлаживать оптимизированный код

У gcc и clang есть опция -Og: эта опция включает все оптимизации, которые не мешают отладке. Если отладочная версия кода работает медленно, попробуйте скомпилировать ее с -Og.

Код с -Og:

X::X(unsigned long, unsigned long):
    mov     rax, rdi
    mov     rdx, rsi
    ret

Ресурсы

Дополнительная информация о -Og и других опциях, облегчающих отладку кода: https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html

Дополнительная информация об оптимизации и опциях оптимизации: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options

1 голос
/ 25 марта 2019

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

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

Эти два фактора в совокупности дают вам код, который вы видите в разборке (хотя clang явно делает лучше, чем gcc здесь).

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

Я не уверен, почему оба компилятора обнуляют a_ и b_ перед тем, как присваивать им в вашей разборке. Я не вижу этого в Godbolt .

...