Чому додавання вбудованих коментарів до збірки спричиняє такі радикальні зміни у коді, що генерується GCC?


82

Отже, у мене був такий код:

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?


28
Я би очікував, що GCC (і більшість компіляторів) будуть розглядати конструкцію ASM як блок-бокси. Тому вони не можуть міркувати про те, що відбувається через таку коробку. І це заважає багатьом оптимізаціям, особливо тим, що переносяться через межі циклу.
Айра Бакстер,

10
Спробуйте розширену asmформу з пустими вихідними даними та списками клоббер.
Kerrek SB

4
@ R.MartinhoFernandes: asm("# im in ur loop" : : );(див. Документацію )
Mike Seymour

16
Зверніть увагу, що ви можете отримати трохи більше допомоги при перегляді згенерованої збірки, додавши -fverbose-asmпрапор, який додає деякі анотації, які допоможуть визначити, як речі рухаються між регістрами.
Метью Слаттері

1
Дуже цікаво. Може використовуватися для вибіркового уникнення оптимізації циклів?
Шепурін

Відповіді:


62

Взаємодія з оптимізацією пояснюється приблизно наполовину "Інструкції асемблера з операндами виразу C" у документації.

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

Зокрема, зверніть увагу:

asmІнструкція без вихідних операндів буде ставитися однаково до летючим asmінструкції.

і

volatileКлючове слово вказує на те, що команда має серйозні побічні ефекти [...]

Отже, наявність asmусередині вашого циклу перешкоджає оптимізації векторизації, оскільки GCC припускає, що він має побічні ефекти.


1
Зверніть увагу, що побічні ефекти оператора Basic Asm не повинні включати модифікацію регістрів або будь-яку пам'ять, яку ваш код C ++ коли-небудь читає / пише. Але так, asmзаява повинна запускатися один раз для кожного разу, коли це було б в машині абстрактних C ++, і GCC вирішив не векторизувати, а потім видавати asm 16 разів поспіль на paddb. Це, на мою думку, було б законно, оскільки доступ до символів не є volatile. (На відміну від розширеного висловлювання asm з "memory"клоббером)
Пітер Кордес,

1
Див. Gcc.gnu.org/wiki/ConvertBasicAsmToExtended з причин, щоб взагалі не використовувати оператори GNU C Basic Asm. Хоча цей варіант використання (просто маркер коментаря) є одним з небагатьох, де його нерозумно спробувати.
Пітер Кордес,

23

Зверніть увагу, що 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 було змінено.


1
gcc справді не розуміє, що знаходиться в блоці asm - ви повинні сказати це за допомогою розширеного оператора asm. без цієї додаткової інформації gcc не рухатиметься по таких блоках. gcc також не плутається у випадках, які ви заявляєте - ви просто зробили помилку при програмуванні, сказавши gcc, що він може використовувати ці регістри, коли насправді ваш код їх розблоковує.
Згадайте Моніку

Пізня відповідь, але я думаю, що варто сказати. volatile asmповідомляє GCC, що код може мати "важливі побічні ефекти", і він буде розглядати це з особливою обережністю. Його все одно можна видалити в рамках оптимізації мертвого коду або перемістити. Взаємодія з кодом С має передбачати такий (рідкісний) випадок і накласти суворе послідовне оцінювання (наприклад, шляхом створення залежностей всередині asm).
edmz

GNU C Basic asm (без обмежень на операнди, як OP asm("")) неявно мінливий, як і Extended asm без вихідних операндів. GCC не розуміє рядок шаблону asm, лише обмеження; ось чому важливо точно і повністю описати своє asm компілятору, використовуючи обмеження. Заміна операндів у рядок шаблону не потребує більше розуміння, ніж printfвикористання рядка форматування. TL: DR: не використовуйте GNU C Basic asm ні для чого, крім випадків використання, подібних до цього, із чистими коментарями.
Пітер Кордес,

-2

Ця відповідь тепер модифікована: вона спочатку була написана з мисленням, розглядаючи вбудований Basic Asm як досить чітко визначений інструмент, але в GCC це нічого подібного. Basic Asm слабкий, тому відповідь відредаговано.

Кожен коментар до асамблеї діє як точка зупинку.

EDIT: Але зламаний, оскільки ви використовуєте Basic Asm. Вбудований asm( asmвираз у тілі функції) без явного списку клоберів є слабко визначеною функцією в GCC, і його поведінку важко визначити. Це не здається , (я не в повній мірі усвідомити свої гарантії) прив'язується ні до чого , зокрема, так в той час як код збірки повинен бути запущений в якій - то момент , якщо функція виконується, то не ясно , коли вона виконується для будь-якої неотрицательной тривіальний рівень оптимізації . Точка зупинки, яку можна впорядкувати за допомогою сусідньої інструкції, не дуже корисна "точка зупинки". END EDIT

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

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

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

Кращим запитанням було б:

Привіт, GCC. Чому ви вважаєте, що цей вихід asm реалізує вихідний код? Будь ласка, пояснюйте поетапно, з кожним припущенням.

Але тоді ви не хотіли б читати доказ довше, ніж висновок asm, написаний у вигляді внутрішнього представлення GCC.


1
Ці точки повинні існувати, щоб ви могли спостерігати за середовищем (стан реєстрів та пам'ять). - це може бути правдою для неоптимізованого коду. Якщо ввімкнено оптимізацію, цілі функції можуть зникнути з двійкового файлу. Тут ми говоримо про оптимізований код.
Bartek Banachewicz

1
Ми говоримо про збірку, створену в результаті компіляції з увімкненими оптимізаціями. Отже, ви помиляєтесь, заявляючи, що щось має існувати.
Bartek Banachewicz

1
Так, IDK, чому б хтось це зробив, і погодьтеся, що ніхто ніколи не повинен. Як пояснюється за посиланням у моєму останньому коментарі, ніхто ніколи цього не повинен робити, і вже "memory"йшли суперечки щодо його посилення (наприклад, неявною клоббер-програмою) як перекладу для існуючого коду глюків, який, безсумнівно, існує. Навіть для таких інструкцій, asm("cli")які впливають лише на частину архітектурного стану, який згенерований компілятором код не торкається, він все одно потрібен упорядкований wrt. згенеровані компілятором навантаження / сховища (наприклад, якщо ви відключаєте переривання навколо критичного розділу).
Пітер Кордес,

1
Оскільки небезпечно клобувати червону зону, навіть неефективне ручне збереження / відновлення реєстрів (за допомогою push / pop) всередині оператора asm не є безпечним, якщо ви не зробите цього add rsp, -128спочатку. Але робити це, очевидно, розумово.
Пітер Кордес,

1
В даний час GCC трактує Basic Asm в точності еквівалентно asm("" :::)(неявно мінливий, оскільки він не має вихідних даних, але не прив'язаний до решти коду вхідними або вихідними залежностями. І немає "memory"клоббера). І, звичайно, це не робить %operandзаміни на рядок шаблону, тому літерал %не потрібно уникати як %%. Тож так, погоджуюсь, знецінення Basic Asm поза __attribute__((naked))функціями та глобальним обсягом було б непоганою ідеєю.
Пітер Кордес,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.