Почему добавление комментариев к сборке вызывает такое радикальное изменение в сгенерированном коде?
Итак, у меня был этот код:
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.