<가 <=보다 빠릅니까?


1574

if( a < 901 )보다 더 빨리 if( a <= 900 ).

이 간단한 예제와 정확히 같지는 않지만 루프 복잡한 코드에서 약간의 성능 변화가 있습니다. 나는 이것이 사실 일 경우에 생성 된 머신 코드로 무언가를해야한다고 생각한다.


153
역사적 중요성, 답변의 질, 성과 의 다른 주요 질문이 계속 남아 있다는 사실을 감안할 때이 질문을 마감 해야하는 이유는 없습니다 (특히 투표가 표시됨에 따라 특히 삭제되지 않아야 함) . 기껏해야 잠겨 있어야합니다. 또한, 질문 자체가 틀린 정보 / 순진한 경우에도, 그것이 책에 나타난 사실은 어딘가에 "신뢰할 수있는"출처에 원래의 잘못된 정보가 존재한다는 것을 의미하며, 따라서이 질문은 그것을 명확하게하는 데 도움이된다는 점에서 건설적인 것입니다.
Jason C

32
당신은 당신이 어떤 책 을 언급하고 있는지 말하지 않았습니다 .
Jonathon Reinhart

159
입력하는 <것이 입력하는 것보다 두 배 빠릅니다 <=.
Deqing

6
8086에 맞았습니다.
Joshua

7
업 보트 수는 과도하게 최적화 된 수백 명의 사람들이 있음을 분명히 보여줍니다.
m93a

답변:


1704

아니요, 대부분의 아키텍처에서 더 빠르지는 않습니다. 지정하지 않았지만 x86에서 모든 적분 비교는 일반적으로 두 개의 기계 명령어로 구현됩니다.

  • test또는 cmp명령어 세트EFLAGS
  • 그리고 비교 유형 및 코드 레이아웃에 따라 Jcc(점프) 명령 :
    • jne -같지 않으면 점프-> ZF = 0
    • jz -0이면 점프 (같음)-> ZF = 1
    • jg -더 큰 경우 점프-> ZF = 0 and SF = OF
    • (기타...)

예제 (간결하게 편집)$ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

컴파일 :

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

    if (a <= b) {
        // Do something 2
    }

컴파일 :

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

따라서 둘 사이의 유일한 차이점은 명령 jgjge명령입니다. 두 사람은 같은 시간이 걸립니다.


다른 점프 명령이 같은 시간이 걸린다는 것을 나타내는 것은 아무것도 없습니다. 이것은 대답하기가 약간 까다 롭지 만 여기에 내가 줄 수있는 것이 있습니다 .Intel Instruction Set Reference 에서는 모두 하나의 공통 명령 Jcc(모두 조건이 충족되면 점프)으로 그룹화됩니다 . 부록 C. 지연 시간 및 처리량 의 최적화 참조 매뉴얼에 동일한 그룹화가 함께 이루어집니다 .

지연 시간 — 실행 코어가 명령을 구성하는 모든 μops의 실행을 완료하는 데 필요한 클럭주기 수입니다.

처리량 — 문제 포트가 동일한 명령을 다시 허용하기 전에 대기하는 데 필요한 클럭주기 수입니다. 많은 명령어의 경우 명령어의 처리량이 지연 시간보다 훨씬 적을 수 있습니다.

Jcc은 다음과 같습니다.

      Latency   Throughput
Jcc     N/A        0.5

다음 각주와 함께 Jcc:

7) 조건부 점프 명령의 선택은 분기의 예측 성을 향상시키기 위해 3.4.1 절.“지점 예측 최적화”섹션의 권장 사항을 기반으로해야합니다. 분기가 성공적으로 예측되면 대기 시간 jcc은 사실상 0입니다.

따라서 인텔 문서에서는 Jcc어떤 명령도 다른 명령과 다르게 처리되지 않습니다 .

명령어를 구현하기 위해 사용 된 실제 회로에 대해 생각한다면 EFLAGS, 조건이 충족되는지 여부를 결정하기 위해 서로 다른 비트에 간단한 AND / OR 게이트가 있다고 가정 할 수 있습니다 . 따라서 2 비트를 테스트하는 명령어가 1 개만 테스트하는 것보다 더 많거나 적은 시간이 걸리는 이유는 없습니다 (클럭주기보다 훨씬 짧은 게이트 전파 지연 무시).


편집 : 부동 소수점

이것은 x87 부동 소수점에도 적용됩니다. (위와 거의 동일하지만 double대신 int.

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

239
@Dyppl 실제로 jgjnle같은 명령입니다 7F:-)
Jonathon Reinhart

17
한 옵션이 다른 옵션보다 빠르면 옵티마이 저가 코드를 수정할 수 있다는 것은 말할 것도 없습니다.
Elazar Leibovich

3
동일한 양의 명령어를 생성한다고해서 반드시 해당 명령어를 모두 실행하는 데 걸리는 총 시간이 동일하다는 것을 의미하지는 않습니다. 실제로 더 많은 명령이 더 빨리 실행될 수 있습니다. 사이클 당 명령어는 고정 된 숫자가 아니며 명령어에 따라 다릅니다.
jontejj

22
@jontejj 나는 그것을 많이 알고 있습니다. 내 대답을 읽었 습니까 ? 나는 동일한 의 명령어 에 대해 아무 것도 언급하지 않았 으며 , 하나의 점프 명령이 하나의 플래그를보고 다른 점프 명령이 두 개의 플래그를보고 있다는 것을 제외하고 는 본질적으로 정확히 동일한 instrutcions 으로 컴파일되었다고 말했습니다 . 나는 그것들이 의미 적으로 동일하다는 것을 보여주기 위해 충분한 증거를 제공했다고 생각합니다.
Jonathon Reinhart 5

2
@jontejj 당신은 아주 좋은 지적을합니다. 이 답변이 얻을 수있는만큼 많은 가시성을 얻으려면 약간 정리해야합니다. 피드백을 주셔서 감사합니다.
Jonathon Reinhart 2018 년

593

역사적으로 (우리는 1980 년대와 1990 년대 초반을 얘기하고), 거기 몇몇 이 사실있는 아키텍처. 근본적인 문제는 정수 비교가 본질적으로 정수 빼기를 통해 구현된다는 것입니다. 이로 인해 다음과 같은 경우가 발생합니다.

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

이제 A < B뺄셈이 고 비트를 빌려야 뺄 때 , 뺄셈이 정확 해지려면, 손으로 더하거나 뺄 때와 같이 빌리십시오. 이 "빌려온"비트는 일반적으로 캐리 비트 라고 하며 분기 명령으로 테스트 할 수 있습니다. 빼기가 같음을 의미하는 동일 제로인 경우 0 비트라는 두 번째 비트 가 설정됩니다.

일반적으로 최소 두 개의 조건부 분기 명령어가있었습니다. 하나는 캐리 비트에서 분기하고 다른 하나는 0 비트에서 분기했습니다.

이제 문제의 핵심을 파악하기 위해 캐리 및 제로 비트 결과를 포함하도록 이전 표를 확장 해 보겠습니다.

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

따라서이 경우 에만A < B 캐리 비트가 명확하기 때문에 분기에 대한 구현은 하나의 명령으로 수행 할 수 있습니다 .

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

그러나 동등하지 않은 비교를 원한다면 평등의 경우를 잡기 위해 제로 플래그를 추가로 확인해야합니다.

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

따라서 일부 기계에서는 "보다 작음"비교를 사용 하면 하나의 기계 명령 저장 될 수 있습니다 . 이는 메가 헤르츠 미만 프로세서 속도와 1 : 1 CPU-to-memory 속도 비율과 관련이 있었지만 오늘날에는 거의 관련이 없습니다.


10
또한 x86과 같은 아키텍처는와 같은 명령어를 구현 jge하여 제로 및 부호 / 캐리 플래그를 모두 테스트합니다.
greyfade

3
주어진 아키텍처에 해당되는 경우에도 마찬가지입니다. 컴파일러 작성자 중 누구도 눈치 채지 못했을 가능성이 높으며 느린 것을 더 빠른 것으로 바꾸는 최적화를 추가 했습니까?
존 한나

8
이것은 8080에서도 마찬가지입니다. 0에 점프하고 마이너스에 점프하라는 지시가 있지만 동시에 테스트 할 수있는 것은 없습니다.

4
이것은 6502 및 65816 프로세서 제품군의 경우에도 해당되며 Motorola 68HC11 / 12에도 적용됩니다.
Lucas

31
심지어 8080 <=시험에서 구현 될 수있다 하나 개의 피연산자가 스와핑을 위해 테스트를 지시 not <(당량 >=이것은 바람직하다) <=교환 피연산자 : cmp B,A; bcs addr.
그것이이

92

내부 정수 유형에 대해 이야기하고 있다고 가정하면 하나가 다른 정수보다 빠를 수있는 방법은 없습니다. 그것들은 의미 상 분명히 동일합니다. 둘 다 컴파일러에게 정확히 동일한 작업을 수행하도록 요청합니다. 끔찍하게 고장난 컴파일러 만이 이들 중 하나에 대해 열등한 코드를 생성합니다.

일부 플랫폼 있었다면 <보다 더 빨리이었다 <=간단한 정수 유형의 컴파일러한다 항상 변환 <=<상수. 나쁜 컴파일러가 아닌 모든 컴파일러 (플랫폼 용).


6
+1 동의합니다. 컴파일러가 원하는 속도를 결정할 때까지 속도 도 <없습니다 <=. 당신은 ... 그들은 일반적으로 이미 죽은 코드 최적화, 꼬리 호출 최적화, (경우에 및 줄이기) 루프 리프팅, 다양한 루프의 자동 parallelisation 등을 실시 할 것을 고려할 때 컴파일러에 대한 매우 간단한 최적화입니다 왜 시간을 낭비 조기 최적화를 숙고 ? 프로토 타입을 실행하고 프로파일 링하여 가장 중요한 최적화가 어디에 있는지 파악하고, 중요도에 따라 최적화를 수행하고 진척도를 측정하는 방식에 따라 프로파일을 다시 수행하십시오 ...
autistic

하나 개의 상수 값을 갖는 비교 느려 될 수 서리 경우 여전히있다 <=, 예를 들면,시의 변형 (a < C)에 대한 (a <= C-1)(일부 상수가 C) 발생 C명령어 세트 인코딩하는데 더 어렵게한다. 예를 들어, 명령어 세트는 비교에서 압축 형식으로 -127에서 128 사이의 부호있는 상수를 나타낼 수 있지만 해당 범위 밖의 상수는 더 길거나 느린 인코딩 또는 다른 명령어를 사용하여로드해야합니다. 따라서 같은 비교 (a < -127)에는 간단한 변환이 없을 수 있습니다.
BeeOnRope

@BeeOnRope이 문제는 상수가 다르기 때문에 다른 연산을 수행하면 성능에 영향을 줄 수 있는지 여부가 아니라 다른 상수를 사용하여 동일한 연산을 표현 하면 성능에 영향을 줄 수 있는지 여부 였습니다. 우리가 비교하지 않을 그래서 에 당신이 거기에 선택의 여지가 없기 때문에, 당신은 당신이 필요로하는 하나를 사용하십시오. 우리는 같은 진리표를 가지고 있기 때문에 다른 인코딩이나 다른 명령을 필요로 할 수 없는와 비교 하고 있습니다. 하나의 인코딩은 다른 인코딩과 동일합니다. a > 127a > 128a > 127a >= 128
David Schwartz

나는 당신의 문이 "컴파일러는 항상 변환해야 [<=이 느렸다 일부 플랫폼이 있었다 경우에 일반적인 방법으로 응답되었다 <=<상수가". 내가 아는 한, 그 변환에는 상수 변경이 포함됩니다. 예를 들어 빠르기 때문에 a <= 42컴파일됩니다 . 일부 엣지의 경우 새로운 상수가 더 많거나 느린 명령을 요구할 수 있기 때문에 그러한 변환은 유익하지 않습니다. 물론 과 동일하고, 컴파일러는 (같은) 가장 빠른 방법은 모두 형태를 인코딩해야하지만, 그게 내가 한 말과 일치하지 않습니다. a < 43<a > 127a >= 128
BeeOnRope

67

둘 다 빠르지 않다는 것을 알았습니다. 컴파일러는 각 조건에서 다른 값으로 동일한 기계 코드를 생성합니다.

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

내 예 if는 Linux의 x86_64 플랫폼에있는 GCC입니다.

컴파일러 작가는 꽤 똑똑한 사람들이며, 이러한 것들과 우리 대부분의 다른 사람들이 당연하다고 생각합니다.

상수가 아닌 경우 두 경우 모두 동일한 머신 코드가 생성됩니다.

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

9
이것은 x86에만 해당됩니다.
Michael Petrotta

10
나는 그것이 if(a <=900)정확히 같은 asm을 생성 한다는 것을 증명하기 위해 그것을 사용해야한다고 생각합니다. :)
Lipis

2
@AdrianCornish 죄송합니다 .. 나는 그것을 편집 .. 그것은 거의 동일합니다 .... 당신이 <= 900이면 두 번째를 변경하면 asm 코드는 정확히 동일합니다 :) 그것은 지금 거의 동일합니다 ..하지만 당신은 알고있다. OCD를 위해 :)
Lipis

3
@Boann 그것은 (true) if로 줄어들고 완전히 제거 될 수 있습니다.
Qsario

5
아무도이 최적화 가 상수 비교에만 적용 된다고 지적하지 않았습니다 . 두 변수를 비교할 때 이와 같이 수행 되지 않도록 보장 할 수 있습니다 .
Jonathon Reinhart

51

부동 소수점 코드의 경우 <= 비교는 현대 아키텍처에서도 실제로 (한 명령으로) 느려질 수 있습니다. 첫 번째 기능은 다음과 같습니다.

int compare_strict(double a, double b) { return a < b; }

PowerPC에서 먼저 부동 소수점 비교 ( cr조건 레지스터 업데이트 )를 수행 한 다음 조건 레지스터를 GPR로 이동하고 "보다 작은 비교"비트를 제자리로 이동 한 다음 리턴합니다. 네 가지 지침이 필요합니다.

이제이 기능을 대신 고려하십시오.

int compare_loose(double a, double b) { return a <= b; }

이를 위해서는 compare_strict위와 동일한 작업이 필요 하지만 이제 "보다 작음"과 "같음"이라는 두 가지 관심 대상이 있습니다. 이 cror두 비트를 하나로 결합 하려면 추가 명령어 ( -조건 레지스터 비트 OR)가 필요합니다 . 따라서 compare_loose5 개의 지침이 compare_strict필요하지만 4 개의 지침이 필요합니다.

컴파일러가 다음과 같이 두 번째 함수를 최적화 할 수 있다고 생각할 수 있습니다.

int compare_loose(double a, double b) { return ! (a > b); }

그러나 이것은 NaN을 잘못 처리합니다. NaN1 <= NaN2그리고 NaN1 > NaN2모두 필요가 false로 평가합니다.


다행히 x86 (x87)에서는 이와 같이 작동하지 않습니다. fucomipZF와 CF를 설정합니다.
Jonathon Reinhart

3
@ JonathonReinhart : PowerPC 가하는 일을 오해하고 있다고 생각합니다. 조건 레지스터 cr x86 ZF과 같은 플래그와 같습니다 CF. (CR이 더 융통성이 있지만) 포스터가 말한 것은 결과를 GPR로 옮기는 것입니다. PowerPC에 대해 두 가지 명령을 수행하지만 x86에는 조건부 이동 명령이 있습니다.
Dietrich Epp

@DietrichEpp 내 진술 이후에 추가하려는 것은 EFLAGS의 가치에 따라 즉시 뛰어 넘을 수 있습니다. 명확하지 않아서 죄송합니다.
Jonathon Reinhart

1
@JonathonReinhart : 예. CR 값을 기준으로 즉시 점프 할 수도 있습니다. 대답은 점프에 대해 말하는 것이 아니며, 추가 지침이 나오는 곳입니다.
Dietrich Epp

34

아마도 그 이름이없는 책의 저자는 읽은 a > 0것보다 더 빨리 읽히고 그것이 a >= 1보편적으로 사실이라고 생각했을 것입니다.

그러나는 0( CMP아키텍처에 따라, 예를 들어로 대체 될 수 있기 때문에)가 포함되어 OR있기 때문이 아니라 때문입니다 <.


1
물론 "디버그"빌드에서는 최적화 프로그램에 의해 후자가 후자로 변환 될 수 있기 때문에 (a >= 1)보다 느린 속도로 실행 하는 데 나쁜 컴파일러가 필요 하다 (a > 0).
BeeOnRope

2
@BeeOnRope 최적화 프로그램이 최적화 할 수있는 복잡한 작업과이를 수행하지 못하는 작업에 놀라기도합니다.
glglgl

1
실제로, 그리고 아주 작은 함수에 대한 asm 출력을 항상 확인할 가치가 있습니다. 즉, 위의 변환은 매우 기본적이고 간단한 컴파일러에서도 수십 년 동안 수행되었습니다.
BeeOnRope

32

적어도 이것이 사실이라면 컴파일러는 <= b를! (a> b)로 사소하게 최적화 할 수 있으므로 비교 자체가 실제로 느리더라도 가장 순수한 컴파일러를 제외하고는 차이를 느끼지 못할 것입니다 .


! (a> b)가 <= b의 최적화 된 버전 인 이유는 무엇입니까? 하나도 아닌! (a> b) 2 작업입니까?
Abhishek Singh

6
@AbhishekSingh는 NOT단지 다른 명령 (의해 이루어진다 jejne)
파벨 Gatnar

15

그들은 같은 속도를 가지고 있습니다. 어쩌면 특별한 아키텍처에서 그가 말한 것은 옳지 만 x86 제품군에서는 적어도 그것들이 동일하다는 것을 알고 있습니다. 이를 위해 CPU는 빼기 (a-b)를 수행 한 다음 플래그 레지스터의 플래그를 확인합니다. 이 레지스터의 두 비트는 ZF (zero Flag) 및 SF (sign flag)라고하며 한 번의 마스크 작업으로 수행되므로 한 주기로 수행됩니다.


14

이것은 C가 컴파일되는 기본 아키텍처에 크게 의존합니다. 일부 프로세서와 아키텍처에는 서로 다른 횟수의 사이클에서 실행되는 것과 같거나 작거나 같은 명시 적 명령이있을 수 있습니다.

그러나 컴파일러가 문제를 해결할 수 있기 때문에 매우 드문 일입니다.


1
고환에 차이가있는 경우. 1) 감지 할 수 없습니다. 2) 소금의 가치가있는 컴파일러는 이미 코드의 의미를 변경하지 않고 느린 형식에서 빠른 형식으로 변환하는 것입니다. 따라서 심은 결과는 동일합니다.
Martin York

완전히 합의하면 어쨌든 아주 사소하고 어리석은 차이가 될 것입니다. 분명히 플랫폼에 구애받지 않는 책에서 언급 할 것은 없습니다.
Telgin

@lttlrck : 알겠습니다. 시간이 좀 걸렸어 측정을 불가능하게하는 다른 많은 것들이 있기 때문에 탐지 할 수 없습니다. 프로세서 정지 / 캐시 누락 / 신호 / 프로세스 교환. 따라서 정상적인 OS 상황에서 단일 사이클 레벨의 물체는 물리적으로 측정 할 수 없습니다. 측정에서 모든 간섭을 제거 할 수 있다면 (온보드 메모리가 있고 칩이없는 칩에서 실행) OS에 대해 걱정해야 할 세분화 된 타이머가 있지만 이론적으로 충분히 오래 실행하면 무언가를 볼 수 있습니다.
Martin York

12

TL; DR 답변

대부분의 아키텍처, 컴파일러 및 언어 조합은 더 빠르지 않습니다.

전체 답변

다른 답변에 집중 한 86 아키텍처, 그리고 내가 모르는 ARM의 생성 된 코드에 특별히 언급을 충분히합니다 (예를 들어, 어셈블러 것 같다) 아키텍처를, 그러나 이것은의 예입니다 마이크로 최적화 입니다 매우 아키텍처 구체적이고 최적화가되는 것처럼 안티-최적화 일 가능성이 높습니다 .

따라서 이런 종류의 마이크로 최적화 는 최고의 소프트웨어 엔지니어링 방식이 아닌 화물 컬트 프로그래밍 의 예 라고 제안합니다 .

이 아마 몇몇 이 최적화입니다 아키텍처는,하지만 난 그 반대가 사실 일 수도 적어도 하나의 아키텍처 알고있다. 유서 깊은 Transputer의 아키텍처에만 기계 코드 명령했다 동등보다 크거나 같음 모든 비교는 이러한 프리미티브에서 구축 할 수 있었다, 그래서.

그럼에도 불구하고, 거의 모든 경우에, 컴파일러는 실제로 어떤 비교도 다른 어떤 것보다 이점이없는 방식으로 평가 명령을 주문할 수 있습니다. 최악의 경우, 피연산자 스택 에서 상위 2 개의 항목을 바꾸려면 리버스 명령어 (REV)를 추가해야 할 수도 있습니다. . 단일 바이트 명령으로 단일 사이클이 실행되었으므로 가능한 가장 작은 오버 헤드가있었습니다.

이와 같은 마이크로 최적화가 최적화 인지 안티 최적화 인지 는 사용중인 특정 아키텍처에 따라 다르므로 일반적으로 아키텍처 별 마이크로 최적화를 사용하는 습관을들이는 것은 나쁜 생각입니다. 그렇지 않으면 본능적으로 부적절 할 때 하나를 사용하십시오. 읽은 책이 옹호하는 것과 정확히 같습니다.


6

차이가 있더라도 차이점을 알 수 없습니다. 게다가 실제로 마법 상수를 사용하지 않는 한 추가 작업을 수행 a + 1하거나 a - 1조건을 유지해야합니다. 매우 나쁜 습관입니다.


1
나쁜 습관은 무엇입니까? 카운터 증가 또는 감소? 그러면 인덱스 표기법을 어떻게 저장합니까?
jcolebrand

5
그는 두 가지 변수 유형을 비교하고 있음을 의미합니다. 물론 루프 또는 무언가에 대한 값을 설정하는 것은 사소한 일입니다. 그러나 x <= y이고 y를 알 수
없으면

@JustinDanielson이 동의했습니다. 추악하고 혼란스러운 말은 말할 것도 없습니다.
Jonathon Reinhart

4

여분의 문자로 인해 코드 처리가 약간 느려지기 때문에 대부분의 스크립팅 언어에서 행이 정확하다고 말할 수 있습니다. 그러나 최고 답변이 지적했듯이 C ++에는 영향을 미치지 않아야하며 스크립팅 언어로 수행되는 작업은 아마도 최적화와 관련이 없을 것입니다.


다소 동의하지 않습니다. 경쟁 프로그래밍에서 스크립팅 언어는 종종 문제에 대한 가장 빠른 솔루션을 제공하지만 올바른 솔루션을 얻으려면 올바른 기술 (읽기 : 최적화)을 적용해야합니다.
Tyler Crompton

3

이 답변을 쓸 때 상수 a < 901vs 의 구체적인 예가 아니라 <vs. <=에 대한 제목 질문 만 보았습니다 a <= 900. 대부분의 컴파일러는 항상 사이의 변환 상수의 크기를 축소 <하고<= 예를 들어, 때문에 86 즉시 피연산자 -128..127에 대한 짧은 1 바이트 인코딩을해야합니다.

ARM 및 특히 AArch64의 경우 즉시 인코딩 할 수있는 것은 좁은 필드를 단어의 임의의 위치로 회전 할 수 있는지에 달려 있습니다. 따라서 cmp w0, #0x00f000인코딩 할 수는 있지만 그렇지 cmp w0, #0x00effff않을 수도 있습니다. 따라서 비교를위한 더 작은 규칙과 컴파일 타임 상수가 항상 AArch64에 적용되는 것은 아닙니다.


<vs. <= 일반적으로 런타임 변수 조건을 포함하여

대부분의 컴퓨터에서 어셈블리 언어로 비교할 때 비교하는 <=비용은< . 이것은 당신이 그것에 분기하거나, 0/1 정수를 만들기 위해 그것을 부울하거나, xless CMOV와 같은 분기없는 선택 연산의 술어로 사용하든 적용됩니다. 다른 답변은 질문 의이 부분만을 다루었습니다.

그러나이 질문은 C ++ 연산자, 최적화 프로그램 의 입력 에 관한 것입니다. 일반적으로 둘 다 동일하게 효율적입니다. 컴파일러가 항상 asm으로 구현 한 비교를 변환 할 수 있기 때문에이 책의 조언은 완전히 허구 적으로 들립니다. 그러나 적어도 하나의 예외가 있습니다.<= 실수로 컴파일러가 최적화 할 수없는 것을 만들 수 있습니다.

루프 조건으로서 컴파일러가 루프가 무한하지 않다는 것을 증명하지 못하게하는 경우와 질적으로 다른 경우 <=가 있습니다. < 이는 자동 벡터화를 비활성화하여 큰 차이를 만들 수 있습니다.

부호없는 오버플로는 부호있는 오버플로 (UB)와 달리 base-2 랩으로 잘 정의되어 있습니다. 부호있는 루프 카운터는 일반적으로 부호없는 오버플로 UB를 기반으로 최적화하는 컴파일러를 통해 이로부터 안전 ++i <= size합니다. 결국에는 항상 거짓이됩니다. ( 모든 C 프로그래머가 정의되지 않은 행동에 대해 알아야 할 것 )

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

컴파일러는 정의 되지 않은 동작을 유발하는 을 제외하고 가능한 모든 입력 값에 대해 C ++ 소스의 (정의되고 법적으로 관찰 가능한) 동작을 유지하는 방식으로 만 최적화 할 수 있습니다 .

(간단한 i <= size문제도 발생할 수 있지만 상한을 계산하는 것은 실수로 신경 쓰지 않아도되지만 컴파일러가 고려해야하는 입력에 대해 무한 루프의 가능성을 실수로 도입하는보다 현실적인 예라고 생각했습니다.)

이 경우 size=0에 리드 upper_bound=UINT_MAX, 그리고 i <= UINT_MAX항상 사실이다. 따라서이 루프는 무한대이며 size=0컴파일러는 프로그래머가 크기 = 0을 전달하려고하지 않더라도 존중해야 합니다. 컴파일러가 size = 0이 불가능하다는 것을 증명할 수있는 호출자에게이 함수를 인라인 할 수 있다면 훌륭합니다 i < size.

asm if(!size) skip the loop; do{...}while(--size);for( i<size )루프 내에서 실제 값이 i필요하지 않은 경우 루프 를 최적화하는 일반적으로 효율적인 방법 중 하나입니다 ( 루프가 항상 "do ... while"스타일 (테일 점프)로 컴파일되는 이유는 무엇입니까? ).

그러나 이것은 무한대 일 수 없지만 {}로 입력 size==0하면 2 ^ n의 반복이 발생합니다. ( for 루프 C 에서 부호없는 정수를 반복 하면 0을 포함한 모든 부호없는 정수에 대해 루프를 표현할 수 있지만 캐리 플래그가 없으면 asm 방식으로는 쉽지 않습니다.)

루프 카운터의 랩 어라운드 (wraparound)가 가능성이 있기 때문에, 현대 컴파일러는 종종 "포기"하고 거의 적극적으로 최적화하지 않습니다.

예 : 1에서 n까지의 정수의 합

unsigned i <= n를 사용하면sum(1 .. n) 가우스 n * (n+1) / 2공식을 기반으로 닫힌 형태로 루프 를 최적화하는 clang의 관용구 인식이 무효화 됩니다.

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

Godbolt 컴파일러 탐색기에서 clang7.0 및 gcc8.2의 x86-64 asm

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

그러나 순진한 버전의 경우 clang에서 바보 루프를 얻습니다.

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC는 닫힌 형식을 사용하지 않으므로 루프 조건을 선택해도 실제로 손상되지 않습니다 . iXMM 레지스터의 요소에서 4 개의 값을 병렬로 실행하여 SIMD 정수 추가로 자동 ​​벡터화됩니다 .

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

또한 평범한 스칼라 루프가있어 매우 작 n거나 무한 루프 경우에 사용한다고 생각합니다 .

BTW에서이 두 루프는 루프 오버 헤드에 대한 명령 (및 Sandybridge 제품군 CPU의 UOP)을 낭비합니다. sub eax,1/ cmp / jcc jnz대신 add eax,1/가 더 효율적입니다. 2 대신 1uop (sub / jcc 또는 cmp / jcc의 매크로 융합 후). 두 루프 후의 코드는 EAX를 무조건 작성하므로 루프 카운터의 최종 값을 사용하지 않습니다.


좋은 예가 있습니다. EFLAGS 사용으로 인한 비 순차적 실행에 대한 잠재적 영향에 대한 다른 의견은 어떻습니까? JB가 JBE보다 더 나은 파이프 라인으로 이어지는 것은 순수한 이론적 인 일입니까, 아니면 실제로 일어날 수 있습니까?
rustyx

@ rustyx : 다른 답변 아래 어딘가에 언급 했습니까? 컴파일러는 부분 플래그 스톨을 유발하는 코드를 생성하지 않으며 C <또는 C에서는 그렇지 않습니다 <=. 그러나 ZF가 설정되어 있거나 (ecx == 0) CF가 설정되어 있으면 (EAX == 1의 비트 3) , test ecx,ecx/ bt eax, 3/ jbe는 점프합니다. 읽은 플래그가 전부가 아니기 때문에 대부분의 CPU에서 부분 플래그가 정지됩니다 마지막 명령에서 플래그를 작성하십시오. Sandybridge 제품군에서는 실제로 멈추지 않으며 병합 Uop 만 삽입하면됩니다. cmp/ test모든 플래그를 쓰지만 btZF는 수정하지 않습니다. felixcloutier.com/x86/bt
Peter Cordes

2

컴퓨터를 만든 사람들이 부울 논리에 나쁜 경우에만. 그들은해서는 안됩니다.

모든 비교 ( >= <= > <)는 동일한 속도로 수행 할 수 있습니다.

모든 비교는, 단지 빼기 (차이)이고 그것이 긍정적 / 부정적인지 보는 것입니다.
(를 msb설정하면 숫자는 음수입니다)

확인하는 방법 a >= b? 하위 긍정적 a-b >= 0인지 확인하십시오 a-b.
확인하는 방법 a <= b? 하위 긍정적 0 <= b-a인지 확인하십시오 b-a.
확인하는 방법 a < b? 하위 음수 a-b < 0인지 확인하십시오 a-b.
확인하는 방법 a > b? 하위 음수 0 > b-a인지 확인하십시오 b-a.

간단히 말해, 컴퓨터는 주어진 op의 후드 아래 에서이 작업을 수행 할 수 있습니다.

a >= b== msb(a-b)==0
a <= b== msb(b-a)==0
a > b== msb(b-a)==1
a < b==msb(a-b)==1

물론 컴퓨터는 실제로 ==0또는 그 ==1중 하나 를 수행 할 필요가 없습니다 .
을 위해 ==0그것은 단지를 반전 할 수 msb회로에서.

어쨌든, 그들은 확실히 롤로 a >= b계산 되지 않았을 것입니다a>b || a==b

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.