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


89

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 는 의견에 약간의 맥락 을 제공했습니다.


7
그건 그렇고, 이것은 Chromium의 일부가 아닙니다. URL에서 추론 할 수 있듯이이 프로젝트는 "제 3 자"프로젝트이며 더 자세히 살펴보면이 코드가 널리 사용되는 범용 압축 라이브러리 인 ZLib에서 가져온 것임을 알 수 있습니다.

1
The funny "do {}" generates better code--- 무엇보다 낫다? 재미있는 while () 또는 지루한 것보다 규칙적인 do {}?
n. '대명사'm.

@ H2CO3 설명을 해주셔서 감사합니다. 질문을 원점에 대해 더 구체적으로 수정했습니다.
Dennis

42
이 코멘트는 18 년 전 Borland와 Sun C 컴파일러 시대에 작성되었습니다. 오늘날 컴파일러와의 관련성은 순전히 우연입니다. do단지 a while가 아닌 이 특정 사용법은 조건부 분기를 피하지 않습니다.
Mark Adler

답변:


108

가장 먼저:

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일반적인 경우 루프가 더 빠를 수있는 이유를 강조 하지만 예제 자체는 최신 컴파일러에 어떤 이점도 제공하지 않는 것 같습니다.

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


12
그렇다면 상태를 한 번만 확인하는 것이 그렇게 큰 이점이 될까요? 나는 그것을 매우 의심한다. 루프를 100 번 실행하면 완전히 무의미 해집니다.

7
@ H2CO3하지만 루프가 한두 번만 실행된다면 어떨까요? 복제 된 조건 코드로 인해 증가 된 코드 크기는 어떻습니까?
Mysticial

6
@Mystical 루프가 한두 번만 실행되는 경우 해당 루프는 최적화 할 가치가 없습니다. 그리고 증가 된 코드 크기는 기껏해야 확실한 주장이 아닙니다. 모든 컴파일러가 여러분이 보여준 방식으로 구현해야하는 것은 아닙니다. 나는 내 장난감 언어에 대한 컴파일러 를 작성했으며 while 루프의 컴파일은 루프의 시작 부분으로 무조건 점프하여 구현되므로 조건에 대한 코드는 한 번만 방출됩니다.

30
@ H2CO3 "루프가 한두 번만 실행되면 해당 루프는 최적화 할 가치가 없습니다." -달라지기를 간청합니다. 다른 루프 안에있을 수 있습니다. 내 자신의 고도로 최적화 된 HPC 코드는 이와 같습니다. 그리고 예, do-while은 차이를 만듭니다.
Mysticial

29
@ H2CO3 내가 그것을 장려한다고 어디에서 말했습니까? 질문은 do-whilewhile 루프보다 빠른 루프입니다. 그리고 나는 그것이 더 빠를 수 있다고 말하면서 질문에 대답했습니다. 나는 얼마나 많이 말하지 않았다. 나는 그것이 가치가 있는지 말하지 않았다. 누구에게도 do-while 루프로 변환을 시작하도록 권장하지 않았습니다. 그러나 단순히 최적화의 가능성이 있다는 것을 부정하는 것은 비록 그것이 작은 것이라 할지라도 제 생각에 이러한 것들에 관심을 갖고 관심이있는 사람들에게 해를 끼치는 것입니다.
Mysticial 2011

24

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

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

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


9
잘 말하면이 문구 premature optimization가 여기에서 떠 오릅니다.
James Snell 2013

@JamesSnell 정확히. 그리고 그것이 최고 평점 답변이 지원 / 장려하는 것입니다.

16
나는 최고 등급의 답변이 조기 최적화를 장려하지 않는다고 생각합니다. 나는 그것이 효율성의 차이가 가능하다는 것을 보여 주지만 그것이 미미하거나 중요하지 않을 수 있다고 주장합니다. 그러나 사람들은 사물을 다르게 해석하고 일부는 필요하지 않을 때 do-while 루프를 사용하기 시작하라는 신호로 볼 수 있습니다 (그렇지 않기를 바랍니다). 어쨌든 지금까지의 모든 답변에 만족합니다. 그들은 질문에 대한 귀중한 정보를 제공하고 흥미로운 토론을 생성했습니다.
Dennis

16

간단히 말해서 (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 내부 최적화를 손상시킬 수 있으므로 결국 적절한 벤치마킹 만 계산됩니다.


레지스터 이동을 저장하는 것 같습니다. mov %rcx,%rsi:) 코드를 재정렬하면 어떻게 할 수 있는지 알 수 있습니다.
Mysticial

@Mystical, 당신은 마이크로 최적화에 대해 옳습니다. 때로는 단일 명령을 저장하는 것조차 가치가 없습니다 (그리고 reg-to-reg 이동은 오늘날 reg 이름을 바꾸면 거의 무료입니다).
Leeor nov.

AMD Bulldozer 및 Intel Ivy Bridge까지 이동 이름 변경이 구현 된 것 같습니다. 놀랍습니다!
Mysticial

@Mysticial, 이들은 대략 물리적 레지스터 파일을 구현하는 첫 번째 프로세서입니다. 오래된 순서가 잘못된 디자인은 레지스터를 재정렬 버퍼에 배치하기 만하면됩니다.
Leeor nov.

3
원래 코드의 주석을 대부분의 것과 다르게 해석 한 것 같습니다. 댓글에는 "the funny do {} .."라고되어 있지만 어떤 버전과 비교해도 웃기지 않은 버전은 나와 있지 않습니다. 대부분의 사람들은 do-while과 while의 차이를 알고 있습니다. 그래서 제 생각에 "the funny do {}"는 그것에 적용되지 않았지만, 당신이 보여준 것처럼 루프 풀기 및 / 또는 추가 할당의 부족에 적용됩니다. 여기.
Abel

10

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.


7

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

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

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

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


그것은 흥미로운 관찰이며 지금까지 모두가 간과 한 것입니다. 그러나 컴파일러는 그것에 영향을 미치지 않습니까? 즉, 어떤 컴파일러를 사용하든 항상 더 효율적입니다. 그렇다면 컴파일러를 언급하는 주석이있는 이유는 무엇입니까?
Dennis

@Dennis : 컴파일러마다 생성 된 코드를 최적화하는 방법이 다릅니다. 일부는 자체적으로 루프 풀기 (일부 확장)를 수행하거나 어웨이 할당을 최적화 할 수 있습니다. 여기서 코더는 컴파일러를 루프 언 롤링으로 강제 실행하여 덜 최적화 된 컴파일러가 여전히 잘 수행되도록합니다. 나는 Yves가 그의 가정에 대해 정확히 옳다고 생각하지만, 원래의 코더가 없었다면 "재미있는"발언 뒤에 진짜 생각이 무엇인지는 약간의 미스터리로 남아 있습니다.
Abel

1
@Abel 명확히 해주셔서 감사합니다. 이제 댓글 뒤에있는 (추정 된) 의미를 더 잘 이해합니다. Yves는 댓글 뒤에 숨겨진 미스터리를 해결하는 데 가장 가까웠지만 Mysticial의 대답은 그가 내 질문에 가장 잘 대답했다고 생각하기 때문에 받아 들일 것입니다. 댓글이 루프 유형에 초점을 맞추도록 오해하게했지만 아마도 조건을 언급하고 있기 때문에 잘못된 질문을 한 것으로 나타났습니다.
Dennis

0

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

while (Condition)
{
}

do
{
}
while (Condition);

절대적으로 동일합니다.

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