IT박스

0과 비교할 때 int 연산자! = 및 ==

itboxs 2020. 10. 19. 07:52
반응형

0과 비교할 때 int 연산자! = 및 ==


! = 및 ==가 0 또는 0이 아닌 것을 테스트하는 가장 빠른 방법이 아니라는 것을 발견했습니다.

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에 대한 어셈블리 출력입니다. 두 비교의 두 번째 버전은 x86-32 및 x86-64 모두에서 ~ 12 % 더 빠릅니다. 그러나 x86-64에서 지침은 동일했지만 (첫 번째 버전은 두 번째 버전과 똑같이 보임) 두 번째 버전은 여전히 ​​더 빠릅니다.

  1. 컴파일러가 x86-32에서 더 빠른 버전을 생성하지 않는 이유는 무엇입니까?
  2. 어셈블리 출력이 동일 할 때 x86-64에서 두 번째 버전이 더 빠른 이유는 무엇입니까?

편집 : 벤치마킹 코드를 추가했습니다. ZERO : 1544ms, 1358ms NON_ZERO : 1544ms, 1358ms 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의 버그 일 수 있습니다.


편집 : 내 코드에 대한 OP의 어셈블리 목록을 보았습니다. 나는 이것이 VS2011일반적인 버그 조차 의심합니다 . 이것은 단순히 OP 코드에 대한 특별한 경우의 버그 일 수 있습니다. 나는 clang 3.2, gcc 4.6.2 및 VS2010으로 OP의 코드를있는 그대로 실행했으며 모든 경우에 최대 차이 는 ~ 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()이는 사용 ==하고 !=, 각각 사업자를 terser와 동일합니다.

Visual Studio 2011은 베타 버전 입니다. 나는 이것을 버그로 생각할 것입니다. 두 개의 다른 컴파일러, 즉 gcc 4.6.2clang 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에 버그로 신고하는 것 입니다.

Note: I compiled them as C source since I don't think using the corresponding C++ compiler would make any significant change here.


This is a great question, but I think you've fallen victim to the compiler's dependency analysis.

The compiler only has to clear the high bits of eax once, and they remain clear for the second version. The second version would have to pay the price to xor eax, eax except that the compiler analysis proved it's been left cleared by the first version.

The second version is able to "cheat" by taking advantage of work the compiler did in the first version.

How are you measuring times? Is it "(version one, followed by version two) in a loop", or "(version one in a loop) followed by (version two in a loop)"?

Don't do both tests in the same program (instead recompile for each version), or if you do, test both "version A first" and "version B first" and see if whichever comes first is paying a penalty.


Illustration of the cheating:

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();

If timer2 duration is less than timer1 duration, we don't conclude that multiplying by 31 is faster than multiplying by 2. Instead, we realize that the compiler performed common subexpression analysis, and the code became:

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

And the only thing proved is that multiplying by 31 is faster than computing common. Which is hardly surprising at all -- multiplication is far far faster than sqrt and exp.

참고URL : https://stackoverflow.com/questions/10838675/int-operators-and-when-comparing-to-zero

반응형