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

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

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?

Ответ 1

Взаимодействия с оптимизациями объясняются примерно на полпути вниз в "Assembler Instructions with C Expression Operands" в документации.

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

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

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

и

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

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

Ответ 2

Обратите внимание, что gcc векторизовал код, разделяя тело цикла на две части, первую обработку 16 элементов за раз, а второй - оставшуюся часть позже.

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Лучший вопрос:

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

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