int оператори! = і == при порівнянні з нулем


75

Я виявив, що! = Та == - не найшвидший спосіб перевірки на нуль чи ненуль.

bool nonZero1 = integer != 0;
xor eax, eax
test ecx, ecx
setne al

bool nonZero2 = integer < 0 || integer > 0;
test ecx, ecx
setne al

bool zero1 = integer == 0;
xor eax, eax
test ecx, ecx
sete al

bool zero2 = !(integer < 0 || integer > 0);
test ecx, ecx
sete al

Компілятор: VC ++ 11 Прапори оптимізації: / O2 / GL / LTCG

Це вихід збірки для x86-32. Другі версії обох порівнянь були на ~ 12% швидшими як для x86-32, так і x86-64. Однак на x86-64 інструкції були однаковими (перші версії виглядали точно так само, як другі версії), але другі версії все-таки були швидшими.

  1. Чому компілятор не генерує швидшу версію на x86-32?
  2. Чому другі версії ще швидші на x86-64, коли вихідні дані збірки однакові?

EDIT: Я додав код тестування. НУЛЬ: 1544 мс, 1358 мс NON_ZERO: 1544 мс, 1358 мс http://pastebin.com/m7ZSUrcP або http://anonymouse.org/cgi-bin/anon-www.cgi/http://pastebin.com/m7ZSUrcP

Примітка: Напевно незручно знаходити ці функції при компіляції в одному вихідному файлі, оскільки main.asm стає досить великим. Я мав zero1, zero2, nonZero1, nonZero2 в окремому вихідному файлі.

EDIT2: Чи міг хтось із встановленими VC ++ 11 та VC ++ 2010 запустити код тестування та опублікувати таймінги? Це дійсно може бути помилка у VC ++ 11.


11
Чи могли б ви надати повну програму, якою ви оцінюєте ефективність?
Джеймс МакНелліс,

То як він гарантує, що решта eax дорівнює нулю, якщо він просто пропускає xor?
harold

1
Звідки беруться xorінструкції? Вони не виглядають релевантними тесту, тому він повинен бути частиною оточуючого коду.
Марк Ренсом

2
Що станеться, якщо ви зміните порядок? Компілятор достатньо розумний, щоб знати, що він був xor: виданий eax до першого тесту і що залишається чинним для наступного ...
Андреас Магнуссон,

2
NFRCR, ти справді порівняв це як лінійний код? Я припускав, що ви просто склеїли їх, щоб зменшити розмір повідомлення.
harold

Відповіді:


19

EDIT: Побачив список збірки OP для мого коду. Я сумніваюся, що це навіть загальна помилка VS2011 зараз. Це може бути просто особлива помилка коду OP. Я запустив код OP як є з clang 3.2, gcc 4.6.2 та VS2010, і у всіх випадках максимальні відмінності становили ~ 1%.

Просто зібрав джерела з відповідними змінами до мого ne.cфайлу /O2та /GLпрапорів та . Ось джерело

int ne1(int n) {
 return n != 0;
 }

 int ne2(int n) {
 return n < 0 || n > 0;
 }

 int ne3(int n) {
 return !(n == 0);
 }

int main() { int p = ne1(rand()), q = ne2(rand()), r = ne3(rand());}

та відповідна збірка:

    ; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.30319.01 

    TITLE   D:\llvm_workspace\tests\ne.c
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB OLDNAMES

EXTRN   @__security_check_cookie@4:PROC
EXTRN   _rand:PROC
PUBLIC  _ne3
; Function compile flags: /Ogtpy
;   COMDAT _ne3
_TEXT   SEGMENT
_n$ = 8                         ; size = 4
_ne3    PROC                        ; COMDAT
; File d:\llvm_workspace\tests\ne.c
; Line 11
    xor eax, eax
    cmp DWORD PTR _n$[esp-4], eax
    setne   al
; Line 12
    ret 0
_ne3    ENDP
_TEXT   ENDS
PUBLIC  _ne2
; Function compile flags: /Ogtpy
;   COMDAT _ne2
_TEXT   SEGMENT
_n$ = 8                         ; size = 4
_ne2    PROC                        ; COMDAT
; Line 7
    xor eax, eax
    cmp eax, DWORD PTR _n$[esp-4]
    sbb eax, eax
    neg eax
; Line 8
    ret 0
_ne2    ENDP
_TEXT   ENDS
PUBLIC  _ne1
; Function compile flags: /Ogtpy
;   COMDAT _ne1
_TEXT   SEGMENT
_n$ = 8                         ; size = 4
_ne1    PROC                        ; COMDAT
; Line 3
    xor eax, eax
    cmp DWORD PTR _n$[esp-4], eax
    setne   al
; Line 4
    ret 0
_ne1    ENDP
_TEXT   ENDS
PUBLIC  _main
; Function compile flags: /Ogtpy
;   COMDAT _main
_TEXT   SEGMENT
_main   PROC                        ; COMDAT
; Line 14
    call    _rand
    call    _rand
    call    _rand
    xor eax, eax
    ret 0
_main   ENDP
_TEXT   ENDS
END

ne2()який використовував <, >а ||оператори явно дорожчі. ne1()і ne3()які використовують оператори ==та, !=відповідно, є більш стисними та еквівалентними.

Visual Studio 2011 знаходиться в бета-версії . Я вважав би це помилкою. Мої тести з двома іншими компіляторами, а саме gcc 4.6.2 і clang 3.2 , з O2перемикачем оптимізації дали абсолютно однакову збірку для всіх трьох тестів (які я мав) на моєму Windows 7. Ось короткий зміст:

$ cat ne.c

#include <stdbool.h>
bool ne1(int n) {
    return n != 0;
}

bool ne2(int n) {
    return n < 0 || n > 0;
}

bool ne3(int n) {
    return !(n != 0);
}

int main() {}

урожайність з gcc:

_ne1:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    testl   %eax, %eax
    setne   %al
    ret
    .cfi_endproc
LFE0:
    .p2align 2,,3
    .globl  _ne2
    .def    _ne2;   .scl    2;  .type   32; .endef
_ne2:
LFB1:
    .cfi_startproc
    movl    4(%esp), %edx
    testl   %edx, %edx
    setne   %al
    ret
    .cfi_endproc
LFE1:
    .p2align 2,,3
    .globl  _ne3
    .def    _ne3;   .scl    2;  .type   32; .endef
_ne3:
LFB2:
    .cfi_startproc
    movl    4(%esp), %ecx
    testl   %ecx, %ecx
    sete    %al
    ret
    .cfi_endproc
LFE2:
    .def    ___main;    .scl    2;  .type   32; .endef
    .section    .text.startup,"x"
    .p2align 2,,3
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB3:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    call    ___main
    xorl    %eax, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE3:

і з дзвінкою:

    .def     _ne1;
    .scl    2;
    .type   32;
    .endef
    .text
    .globl  _ne1
    .align  16, 0x90
_ne1:
    cmpl    $0, 4(%esp)
    setne   %al
    movzbl  %al, %eax
    ret

    .def     _ne2;
    .scl    2;
    .type   32;
    .endef
    .globl  _ne2
    .align  16, 0x90
_ne2:
    cmpl    $0, 4(%esp)
    setne   %al
    movzbl  %al, %eax
    ret

    .def     _ne3;
    .scl    2;
    .type   32;
    .endef
    .globl  _ne3
    .align  16, 0x90
_ne3:
    cmpl    $0, 4(%esp)
    sete    %al
    movzbl  %al, %eax
    ret

    .def     _main;
    .scl    2;
    .type   32;
    .endef
    .globl  _main
    .align  16, 0x90
_main:
    pushl   %ebp
    movl    %esp, %ebp
    calll   ___main
    xorl    %eax, %eax
    popl    %ebp
    ret

Моя пропозиція полягала б у тому, щоб подати це повідомлення про помилку в Microsoft Connect .

Примітка: Я скомпілював їх як джерело на С, оскільки не думаю, що використання відповідного компілятора С ++ зробить тут якісь суттєві зміни.


1
Ваш новий тест зіпсований, компілятор виконував постійне поширення, оскільки це визначалося n = 10завжди. А потім, крім цього, він повністю виключив виклики функції, оскільки результат не був використаний і немає побічних ефектів.
Бен Войгт,

1
@dirkgently: Коли справа стосується питань оптимізатора, контекст - це все.
Бен Войгт,

7
ЦЕ НЕ ПОМИЛКА! Як це може бути помилка, якщо скомпільований код поводиться як слід? Це показує, що в оптимізаторі є можливості для вдосконалення, але в кожного оптимізатора є можливість для вдосконалення. (Це, до речі, теорема.)
TonyK

1
Про помилки Visual C ++ можуть повідомлятися на Microsoft Connect .
Джеймс МакНелліс,

1
Крім того, варто було б протестувати це за допомогою Visual C ++ 2012 RC, який щойно вийшов сьогодні.
Джеймс МакНелліс,

122

Це чудове питання, але я думаю, що ви стали жертвою аналізу залежності компілятора.

Компілятору потрібно лише eaxодин раз очистити старші біти , а для другої версії вони залишаються чіткими. За другу версію доведеться заплатити ціну, за xor eax, eaxвинятком того, що аналіз компілятора довів, що вона була очищена першою версією.

Друга версія здатна "обдурити", скориставшись роботою, виконаною компілятором у першій версії.

Як ви вимірюєте час? Це "(версія перша, за якою слідує версія друга) у циклі", або "(версія перша у циклі), а потім (версія друга у циклі)"?

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


Ілюстрація шахрайства:

timer1.start();
double x1 = 2 * sqrt(n + 37 * y + exp(z));
timer1.stop();
timer2.start();
double x2 = 31 * sqrt(n + 37 * y + exp(z));
timer2.stop();

Якщо timer2тривалість менше timer1тривалості, ми не робимо висновку, що множення на 31 швидше, ніж множення на 2. Натомість ми розуміємо, що компілятор виконав загальний підвиразний аналіз, і код став:

timer1.start();
double common = sqrt(n + 37 * y + exp(z));
double x1 = 2 * common;
timer1.stop();
timer2.start();
double x2 = 31 * common;
timer2.stop();

І єдиним доведеним є те, що множення на 31 швидше, ніж обчислення common. Що навряд чи дивно - множення набагато швидше, ніж sqrtі exp.


Додано контрольний код. Я провів бенчмарк1 та бенчмарк2 окремо, однакові результати. Єдина відмінність - це перший орієнтир, який працює, потім він «нагрівається» і стає трохи повільнішим.
NFRCR

Це дещо не пов’язано, але хіба компілятор не оптимізував би множення на 31 на a (common << 5) - common?
Метт,

3
@Matt: Не для множення з плаваючою крапкою це не буде;) Для цілочисельного множення, так, я думаю, більшість компіляторів знають цю хитрість, але залежно від архітектури це може бути, а може і не бути швидшим. IMUL на два майже напевно перетворюється на лівий зсув.
Бен Войгт,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.