[c] 컴파일러가 do-while 루프에 대해 다른 유형의 루프에 비해 더 나은 코드를 생성합니까?

zlib 압축 라이브러리 (Chromium 프로젝트에서 사용됨)에는 C의 do-while 루프가 대부분의 컴파일러에서 “더 나은”코드를 생성 함을 암시 하는 주석이 있습니다 . 다음은 표시되는 코드 스 니펫입니다.

do {
} while (*(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         scan < strend);
/* The funny "do {}" generates better code on most compilers */

https://code.google.com/p/chromium/codesearch#chromium/src/third_party/zlib/deflate.c&l=1225

대부분의 (또는 모든) 컴파일러가 더 나은 (예 : 더 효율적인) 코드를 생성 할 것이라는 증거가 있습니까?

업데이트 : 원저자 중 한 명인 Mark Adler 는 의견에 약간의 맥락 을 제공했습니다.



답변

가장 먼저:

do-while루프는 동일하지 않다 while-loop 또는 for-loop.

  • while그리고 for루프는 전혀 루프 본문을 실행되지 않을 수 있습니다.
  • do-while루프는 항상 적어도 한 번 루프 본문을 실행 – 그것은 초기 조건 확인을 건너 뜁니다.

이것이 논리적 차이입니다. 즉, 모든 사람이 이것을 엄격하게 고수하는 것은 아닙니다. 항상 적어도 한 번은 반복되는 것이 보장되는 경우에도 for while또는 for루프를 사용하는 것은 매우 일반적입니다 . (특히 foreach 루프가 있는 언어에서 )

따라서 사과와 오렌지를 비교하지 않으려면 루프가 항상 한 번 이상 실행된다고 가정하겠습니다. 또한 for루프는 본질적 while으로 루프 카운터에 대해 약간의 구문 설탕이있는 루프 이기 때문에 다시 언급하지 않겠습니다 .

그래서 나는 질문에 답할 것입니다.

while루프가 한 번 이상 루프되는 것이 보장되는 경우 do-while대신 루프 를 사용하여 성능이 향상됩니까 ?


A do-while는 첫 번째 조건 확인을 건너 뜁니다. 따라서 평가할 분기와 조건이 하나 더 적습니다.

조건을 확인하는 데 비용이 많이 들고 한 번 이상 반복 할 수 있다는 것을 알고 있다면 do-while루프가 더 빠를 수 있습니다.

그리고 이것은 기껏해야 마이크로 최적화로 간주되지만 컴파일러가 항상 할 수있는 것은 아닙니다. 특히 컴파일러가 루프가 항상 적어도 한 번 입력된다는 것을 증명할 수없는 경우입니다.


즉, while-loop :

while (condition){
    body
}

다음과 같이 효과적으로 동일합니다.

if (condition){
    do{
        body
    }while (condition);
}

항상 적어도 한 번은 반복 할 것이라는 것을 알고 있다면 해당 if 문은 관련이 없습니다.


마찬가지로 어셈블리 수준에서 이것은 대략 다른 루프가 다음과 같이 컴파일되는 방식입니다.

do-while 루프 :

start:
    body
    test
    conditional jump to start

while-loop :

    test
    conditional jump to end
start:
    body
    test
    conditional jump to start
end:

조건이 중복되었습니다. 다른 방법은 다음과 같습니다.

    unconditional jump to end
start:
    body
end:
    test
    conditional jump to start

… 추가 점프를 위해 중복 코드를 교환합니다.

어느 쪽이든 일반 do-while루프 보다 여전히 더 나쁩니다 .

즉, 컴파일러는 원하는 것을 할 수 있습니다. 그리고 그들이 루프가 항상 한 번만 들어간다는 것을 증명할 수 있다면 그것은 당신을 위해 일한 것입니다.


그러나 질문의 ​​특정 예제에는 빈 루프 본문이 있기 때문에 상황이 약간 이상합니다. 본문이 없기 때문에 while와 사이에 논리적 차이가 없습니다 do-while.

FWIW, Visual Studio 2012에서 이것을 테스트했습니다.

  • 본문이 비어 있으면 실제로 while및에 대해 동일한 코드를 생성합니다 do-while. 따라서 그 부분은 컴파일러가 그다지 좋지 않았던 옛날의 남은 부분 일 것입니다.

  • 그러나 비어 있지 않은 본문을 사용하면 VS2012는 조건 코드의 중복을 방지하지만 여전히 추가 조건부 점프를 생성합니다.

따라서 질문의 예제가 do-while일반적인 경우 루프가 더 빠를 수있는 이유를 강조 하지만 예제 자체는 최신 컴파일러에 어떤 이점도 제공하지 않는 것 같습니다.

댓글이 얼마나 오래되었는지를 고려하면 왜 그것이 중요한지 추측 할 수 있습니다. 당시 컴파일러가 본문이 비어 있음을 인식하지 못했을 가능성이 매우 높습니다. (또는 그렇게했다면 정보를 사용하지 않았습니다.)


답변

대부분의 (또는 모든) 컴파일러가 더 나은 (예 : 더 효율적인) 코드를 생성 할 것이라는 증거가 있습니까?

별로, 당신은보고하지 않는 한 실제 조립의 생성 실제, 특정 컴파일러 A의 특정 플랫폼 일부 특정 최적화 설정.

이것은 아마도 수십 년 전 (ZLib이 작성되었을 때)에 대해 걱정할 가치가 있었을 것입니다. 그러나 실제 프로파일 링을 통해 이것이 여러분의 코드에서 병목 현상을 제거한다는 사실 을 발견하지 않는 한 오늘날에는 확실히 아닙니다 .


답변

간단히 말해서 (tl; dr) :

나는 OP의 코드에있는 주석을 약간 다르게 해석하고 있는데, 그들이 관찰했다고 주장하는 “더 나은 코드”는 실제 작업을 루프 “조건”으로 이동했기 때문이라고 생각합니다. 그러나 나는 그것이 매우 컴파일러에 특화되어 있고 그들이 만든 비교가 약간 다른 코드를 생성 할 수는 있지만 아래에서 볼 수 있듯이 대부분 무의미하고 아마도 쓸모가 없다는 것에 완전히 동의합니다.


세부:

do {} while더 나은 코드를 생성하는 것에 대한 그의 코멘트에서 원저자가 의미 한 바를 말하기는 어렵지만 여기서 제기 된 것과는 다른 방향으로 추측하고 싶습니다. 우리는 루프 do {} whilewhile {}루프 의 차이 가 매우 희박 하다고 믿습니다. Mystical은 말 했음),하지만이 코드에는 “더 재밌는”무언가가 있는데, 모든 작업을이 미친 상태에 넣고 내부 부분을 비워 둡니다 ( do {}).

gcc 4.8.1 (-O3)에서 다음 코드를 시도했는데 흥미로운 차이점이 있습니다.

#include "stdio.h" 
int main (){
    char buf[10];
    char *str = "hello";
    char *src = str, *dst = buf;

    char res;
    do {                            // loop 1
        res = (*dst++ = *src++);
    } while (res);
    printf ("%s\n", buf);

    src = str;
    dst = buf;
    do {                            // loop 2
    } while (*dst++ = *src++);
    printf ("%s\n", buf);

    return 0;
}

컴파일 후-

00000000004003f0 <main>:
  ...
; loop 1
  400400:       48 89 ce                mov    %rcx,%rsi
  400403:       48 83 c0 01             add    $0x1,%rax
  400407:       0f b6 50 ff             movzbl 0xffffffffffffffff(%rax),%edx
  40040b:       48 8d 4e 01             lea    0x1(%rsi),%rcx
  40040f:       84 d2                   test   %dl,%dl
  400411:       88 16                   mov    %dl,(%rsi)
  400413:       75 eb                   jne    400400 <main+0x10>
  ...
;loop 2
  400430:       48 83 c0 01             add    $0x1,%rax
  400434:       0f b6 48 ff             movzbl 0xffffffffffffffff(%rax),%ecx
  400438:       48 83 c2 01             add    $0x1,%rdx
  40043c:       84 c9                   test   %cl,%cl
  40043e:       88 4a ff                mov    %cl,0xffffffffffffffff(%rdx)
  400441:       75 ed                   jne    400430 <main+0x40>
  ...

따라서 첫 번째 루프는 7 개의 명령을 수행하고 두 번째 루프는 동일한 작업을 수행해야하지만 6 개의 명령을 수행합니다. 자, 나는 이것 뒤에 컴파일러 똑똑 함이 있는지 실제로 말할 수 없으며 아마도 우연 일뿐이지만이 프로젝트가 사용하는 다른 컴파일러 옵션과 어떻게 상호 작용하는지 확인하지 않았습니다.


반면 clang 3.3 (-O3)에서는 두 루프 모두 다음 5 개의 명령어 코드를 생성합니다.

  400520:       8a 88 a0 06 40 00       mov    0x4006a0(%rax),%cl
  400526:       88 4c 04 10             mov    %cl,0x10(%rsp,%rax,1)
  40052a:       48 ff c0                inc    %rax
  40052d:       48 83 f8 05             cmp    $0x5,%rax
  400531:       75 ed                   jne    400520 <main+0x20>

이는 컴파일러가 상당히 다르며 일부 프로그래머가 몇 년 전에 예상했던 것보다 훨씬 빠른 속도로 발전하고 있음을 보여줍니다. 또한이 댓글은 의미가없고 아직 이해가되는지 아무도 확인하지 않았기 때문에 아마 거기에있을 것입니다.


요점-가능한 최상의 코드로 최적화하고 싶다면 (그리고 그것이 어떻게 생겼는지 알고 있다면) 어셈블리에서 직접 수행하고 방정식에서 “중간자”(컴파일러)를 잘라내십시오. 그러나 그 최신 코드를 고려하십시오. 컴파일러와 최신 하드웨어는이 최적화를 쓸모 없게 만들 수 있습니다. 대부분의 경우 컴파일러가 해당 수준의 작업을 수행하도록하고 큰 작업을 최적화하는 데 집중하는 것이 훨씬 낫습니다.

해야 할 또 다른 요점-명령 수 (원래 OP 코드가 이후에 있었던 것으로 가정)는 결코 코드 효율성에 대한 좋은 측정이 아닙니다. 모든 명령어가 동일하게 생성 된 것은 아니며 일부 (예 : 간단한 reg-to-reg 이동)는 CPU에 의해 최적화되기 때문에 정말 저렴합니다. 다른 최적화는 실제로 CPU 내부 최적화를 손상시킬 수 있으므로 결국 적절한 벤치마킹 만 계산됩니다.


답변

while루프는 종종로 컴파일 된 do-while상태, 즉에 초기 지점으로 루프

    bra $1    ; unconditional branch to the condition
$2:
    ; loop body
$1:
    tst <condition> ; the condition
    brt $2    ; branch if condition true

do-while루프 의 컴파일은 초기 분기없이 동일합니다. while()초기 지점의 비용으로 인해 본질적으로 효율성이 떨어지는 것을 알 수 있지만 한 번만 지불됩니다. [ while,반복마다 조건부 분기와 무조건 분기가 모두 필요한 순진한 구현 방법과 비교하십시오 .]

그러나 그들은 실제로 비교할만한 대안이 아닙니다. while루프를 do-while루프로 또는 그 반대로 변환하는 것은 고통 스럽습니다 . 그들은 다른 일을합니다. 이 경우 여러 메서드 호출이 컴파일러가 수행 한 작업 while을 완전히 지배합니다.do-while.


답변

설명은 제어문 (do vs. while)의 선택에 관한 것이 아니라 루프 풀기에 관한 것입니다 !!!

보시다시피 이것은 문자열 비교 함수 (문자열 요소 길이가 2 바이트 일 수 있음)로, 바로 가기 및 표현식에서 4 개가 아닌 단일 비교로 작성 될 수 있습니다.

이 후자의 구현은 4 개 요소 비교 후 문자열 끝 조건을 한 번 확인하는 반면 표준 코딩은 비교 당 한 번 확인하므로 확실히 더 빠릅니다. 다르게 말하면 4 개 요소 당 5 개의 테스트와 4 개 요소 당 8 개의 테스트가 있습니다.

어쨌든 문자열 길이가 4의 배수이거나 센티넬 요소가있는 경우에만 작동합니다 (두 문자열이 strend테두리를 지나서 달라지는 것을 보장합니다 ). 꽤 위험합니다!


답변

while vs. do 효율성에 대한이 논의는 본문이 없기 때문에이 경우에는 완전히 무의미합니다.

while (Condition)
{
}

do
{
}
while (Condition);

절대적으로 동일합니다.


답변