Int operator!= и == при сравнении с нулем

Я обнаружил, что!= и == не самые быстрые способы тестирования для нуля или ненулевого.

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

Компилятор: VС++ 11 Флаги оптимизации:/O2/GL/LTCG

Это сборка для x86-32. Вторая версия обоих сравнений была примерно на 12% быстрее как на x86-32, так и на x86-64. Тем не менее, на x86-64 инструкции были идентичны (первые версии выглядели точно так же, как и во второй версии), но вторые версии были еще быстрее.

  • Почему компилятор не генерирует более быструю версию на x86-32?
  • Почему вторая версия еще быстрее на x86-64, когда вывод сборки идентичен?

EDIT: я добавил код бенчмаркинга. ZERO: 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: может ли кто-то с установленными VС++ 11 и VС++ 2010 запустить код сравнения и опубликовать тайминги? Это может быть ошибка в VС++ 11.

Ответ 1

РЕДАКТИРОВАТЬ: распечатать список сборок OP для моего кода. Я сомневаюсь, что это даже общая ошибка с VS2011. Это может быть просто ошибка конкретного случая для кода OP. Я запускал OP-код as-is с 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   @[email protected]: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:

и с clang:

    .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.

Примечание. Я скомпилировал их как источник C, так как я не думаю, что использование соответствующего компилятора С++ приведет к значительным изменениям здесь.

Ответ 2

Это отличный вопрос, но я думаю, что вы стали жертвой анализа зависимости компилятора.

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

Вторая версия способна "обмануть", воспользовавшись работой, выполненной компилятором в первой версии.

Как вы измеряете время? "Это" (версия 1, а затем вторая версия) в цикле "или" (первая версия в цикле), за которой следует (вторая версия в цикле) "?

Не выполняйте оба теста в одной и той же программе (вместо этого перекомпилируйте для каждой версии), или если вы это сделаете, сначала проверьте "сначала версию" и "версию 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.