Почему добавление комментариев к сборке вызывает такое радикальное изменение в сгенерированном коде?

Итак, у меня был этот код:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        *sum++ = *a++ + *b++;
    }
}

Я хотел увидеть код, который сгенерирует GCC 4.7.2. Итак, я побежал g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11 И получил следующий вывод:

        .file   "a.c++"
        .intel_syntax noprefix
        .text
        .p2align 4,,15
        .globl  _Z2f1PcS_S_
        .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L5
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L5
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L3:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L3
        mov     ax, 8
        mov     r9d, 992
.L2:
        sub     eax, 1
        lea     rcx, [rdx+r9]
        add     rdi, r9
        lea     r8, [rax+1]
        add     rsi, r9
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L4:
        movzx   edx, BYTE PTR [rcx+rax]
        add     dl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], dl
        add     rax, 1
        cmp     rax, r8
        jne     .L4
        rep
        ret
.L5:
        mov     eax, 1000
        xor     r9d, r9d
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
        .p2align 4,,15
        .globl  _Z2f2PcS_S_
        .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L19
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L19
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L17:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L17
        add     rdi, 992
        add     rsi, 992
        add     rdx, 992
        mov     r8d, 8
.L16:
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L18:
        movzx   ecx, BYTE PTR [rdx+rax]
        add     cl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, r8
        jne     .L18
        rep
        ret
.L19:
        mov     r8d, 1000
        jmp     .L16
        .cfi_endproc
.LFE1:
        .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
        .ident  "GCC: (GNU) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

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

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        asm("# im in ur loop");
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        asm("# im in ur loop");
        *sum++ = *a++ + *b++;
    }
}

И GCC выплюнул это:

    .file   "a.c++"
    .intel_syntax noprefix
    .text
    .p2align 4,,15
    .globl  _Z2f1PcS_S_
    .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L2:
#APP
# 4 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L2
    rep
    ret
    .cfi_endproc
.LFE0:
    .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
    .p2align 4,,15
    .globl  _Z2f2PcS_S_
    .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L6:
#APP
# 12 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L6
    rep
    ret
    .cfi_endproc
.LFE1:
    .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

Это значительно короче и имеет некоторые существенные различия, такие как отсутствие инструкций SIMD. Я ожидал того же результата, с некоторыми комментариями где-то посередине. Я делаю некоторые неправильные предположения здесь? Не мешает ли оптимизатору GCC комментарии asm?

4 ответа

Решение

Взаимодействие с оптимизацией объясняется примерно на полпути вниз по странице "Инструкции ассемблера с операндами выражения C" в документации.

GCC не пытается понять какую-либо фактическую сборку внутри asm; единственное, что он знает о содержимом, - это то, что вы (необязательно) сообщаете ему в спецификации выходных и входных операндов и списке сбоев регистра.

В частности, обратите внимание:

asm Инструкция без каких-либо выходных операндов будет обрабатываться идентично asm инструкция.

а также

volatile Ключевое слово указывает, что инструкция имеет важные побочные эффекты [...]

Так что присутствие asm внутри вашего цикла была запрещена оптимизация векторизации, потому что GCC предполагает, что она имеет побочные эффекты.

Обратите внимание, что gcc векторизовал код, разделив тело цикла на две части, первая обрабатывает 16 элементов за раз, а вторая делает остаток позже.

Как прокомментировала Ира, компилятор не анализирует блок asm, поэтому он не знает, что это просто комментарий. Даже если это так, у него нет возможности узнать, что вы намеревались. У optmized петли тело удвоено, должно ли это поместить ваш асм в каждую? Хотели бы вы, чтобы это не выполнялось 1000 раз? Он не знает, поэтому он идет по безопасному пути и возвращается к простому одиночному циклу.

Я не согласен с "GCC не понимает, что находится в asm() block". Например, gcc вполне может справиться с оптимизацией параметров и даже с перестановкой asm() блоки, такие, что это смешивается с сгенерированным кодом C. Вот почему, если вы посмотрите на встроенный ассемблер, например, в ядре Linux, он почти всегда имеет префикс __volatile__ чтобы убедиться, что компилятор "не перемещает код". Я заставил gcc переместить мой "rdtsc", что позволило мне измерить время, необходимое для выполнения определенной операции.

Как задокументировано, gcc обрабатывает определенные типы asm() блоки как "специальные", и, таким образом, не оптимизируют код по обе стороны блока.

Это не означает, что gcc иногда не будет путаться со встроенными блоками ассемблера или просто решит отказаться от какой-то конкретной оптимизации, потому что он не может следовать за последствиями кода ассемблера и т. Д., И т. Д. Более важно, он часто может быть сбит с толку отсутствующими тэгами clobber - так что если у вас есть какие-то инструкции, такие как cpuid это меняет значение EAX-EDX, но вы написали код так, что он использует только EAX, компилятор может хранить вещи в EBX, ECX и EDX, и тогда ваш код будет очень странным, когда эти регистры будут перезаписаны... Если вам повезло, он сразу падает - тогда легко понять, что происходит. Но если вам не повезло, то он рухнет далеко вниз... Еще одна хитрость - это инструкция деления, которая дает второй результат в edx. Если вы не заботитесь о модуле, легко забыть, что EDX был изменен.

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

Без комментария точка наблюдения не существует, и цикл компилируется как единая математическая функция, которая принимает среду и создает измененную среду.

Вы хотите знать ответ на бессмысленный вопрос: вы хотите знать, как каждая инструкция (или, может быть, блок, или может быть диапазон инструкций) компилируется, но ни одна изолированная команда (или блок) не компилируется; весь материал собран в целом.

Лучший вопрос будет:

Привет GCC. Почему вы считаете, что этот вывод asm реализует исходный код? Пожалуйста, объясните шаг за шагом, с каждым предположением.

Но тогда вы не захотите читать доказательство дольше, чем вывод asm, написанный в терминах внутреннего представления GCC.

Другие вопросы по тегам