카운트 다운하는 것보다 카운트 다운하는 것이 더 빠릅니까?


131

우리의 컴퓨터 과학 교사는 어떤 이유로 든 카운트 다운보다 카운트 다운하는 것이 더 효율적이라고 말했습니다. 예를 들어 FOR 루프를 사용해야하고 루프 인덱스가 어딘가에 사용되지 않는 경우 (N * 행을 화면에 인쇄하는 것과 같이) 다음과 같은 코드를 의미합니다.

for (i = N; i >= 0; i--)  
  putchar('*');  

보다 낫다:

for (i = 0; i < N; i++)  
  putchar('*');  

정말 사실입니까? 그렇다면 왜 그런지 아는 사람이 있습니까?


6
어느 컴퓨터 과학자? 어떤 출판물에서?
bmargulies

26
반복마다 나노초를 절약하거나 양털 매머드 가족의 머리카락을 1 개 정도 절약 할 수 있습니다. 는 putchar시간 (제공하거나 테이크)의 99.9999 %를 사용하고 있습니다.
Mike Dunlavey

38
조기 최적화는 모든 악의 근원입니다. (당신이 이미 알고 있듯이) 그것들은 논리적으로 동일하기 때문에 당신에게 맞는 형태를 사용하십시오. 프로그래밍의 가장 어려운 부분은 프로그램의 이론을 다른 프로그래머 (및 자신에게)에게 전달하는 것입니다. 당신이나 다른 프로그래머가 1 초 이상 그것을 보는 구조를 사용하는 것은 순 손실입니다. "이것이 왜 카운트 다운을 하는가?"
David M

61
첫 번째 루프는 putchar를 11 번 호출하고 두 번째 루프는 10 번만 호출하기 때문에 속도가 느립니다.
Paul Kuliniewicz 2016 년

17
i부호가없는 경우 첫 번째 루프가 무한 루프 라는 것을 알고 계셨습니까 ?
Shahbaz

답변:


371

정말 사실입니까? 그렇다면 그 이유를 아는 사람이 있습니까?

고대에는 컴퓨터가 여전히 용융 실리카에서 손으로 찢어 졌을 때, 8 비트 마이크로 컨트롤러가 지구를 돌아 다닐 때, 그리고 교사가 어렸을 때 (또는 교사가 어렸을 때) 감소와 건너 뛰기 라는 일반적인 기계 명령이있었습니다. 0이면 (DSZ). 핫샷 어셈블리 프로그래머는이 명령어를 사용하여 루프를 구현했습니다. 나중에 기계는 더 멋진 지침을 얻었지만 여전히 다른 것과 비교하는 것보다 0과 무언가를 비교하는 것이 훨씬 저렴한 프로세서가 여전히 많았습니다. 전체 레지스터를 항상 0으로 예약하는 PPC 또는 SPARC와 같은 일부 최신 RISC 시스템에서도 마찬가지입니다.

따라서 루프 대신을 0 대신 비교하도록 조작하면 N어떻게 될까요?

  • 레지스터를 저장할 수 있습니다
  • 작은 이진 인코딩으로 비교 명령을 얻을 수 있습니다
  • 이전 명령에서 플래그를 설정 한 경우 (x86 제품군 시스템에서만 가능) 명시적인 비교 명령이 필요하지 않을 수도 있습니다.

이러한 차이 로 인해 현대의 ​​비 순차적 프로세서 에서 실제 프로그램이 개선 될 가능성이 있습니까? 거의 없습니다. 실제로, 당신이 마이크로 벤치 마크에서도 측정 가능한 개선점을 보여줄 수 있다면 감동받을 것입니다.

요약 : 선생님을 머리 위로 거꾸로!습니다! 루프를 구성하는 방법에 대한 구식 의사 사실을 배우지 않아야합니다. 당신은 학습해야 루프에 대한 가장 중요한 것은 그들이 있는지 확인하는 것입니다 종료 , 생산 정답을 하고있는 읽기 쉬운 . 선생님이 신화가 아닌 중요한 것들에 집중하기를 바랍니다.


3
++ 게다가 putchar루프 오버 헤드보다 많은 시간이 걸립니다.
Mike Dunlavey

41
엄밀한 신화는 아닙니다. 만약 그가 어떤 종류의 uber에 최적화 된 실시간 시스템을하고 있다면, 그것은 편리 할 것입니다. 그러나 이런 종류의 해커는 아마도이 모든 것을 이미 알고있을 것이며 아마도 엔트리 레벨 CS 학생들과 arcana를 혼동하지 않을 것입니다.
Paul Nathan

4
@Joshua :이 최적화는 어떤 방식으로 감지 될 수 있습니까? 질문자가 말했듯이 루프 색인은 루프 자체에서 사용되지 않으므로 반복 횟수가 동일하면 동작에 변화가 없습니다. 정확성 증명의 관점에서 변수 대체 j=N-i는 두 루프가 동일하다는 것을 보여줍니다.
psmears

7
요약은 +1입니다. 현대 하드웨어에서는 사실상 차이가 없으므로 땀을 흘리지 마십시오. 20 년 전과 거의 차이가 없었습니다. 주의를 기울여야한다고 생각되면 두 가지 방법으로 시간을 정하고 명확한 차이를 보지 말고 코드를 명확하고 정확하게 작성하십시오 .
Donal Fellows

3
본문에 대한 투표 또는 요약에 대한 투표를해야하는지 모르겠습니다.
Danubian Sailor

29

컴파일러가 사용하는 숫자의 범위에 대해 추론 할 수있는 것에 따라 일부 하드웨어에서 발생할 수있는 사항은 다음과 같습니다. 증가 루프를 사용 i<N하면 루프 라운드마다 매번 테스트해야합니다 . 감소 버전의 경우 캐리 플래그 (빼기의 부작용으로 설정)가 자동으로을 알려줍니다 i>=0. 루프에서 시간당 테스트를 저장합니다.

실제로 최신 파이프 라인 프로세서 하드웨어에서는 명령에서 클럭 사이클에 대한 간단한 1-1 매핑이 없기 때문에 이러한 사항은 거의 관련이 없습니다. (마이크로 컨트롤러에서 정확한 시간에 비디오 신호를 생성하는 것과 같은 일을하고 있다면 어쨌든 올 수 있다고 상상할 수는 있지만 어쨌든 어셈블리 언어로 작성합니다.)


2
캐리 플래그가 아닌 제로 플래그가 아닌가?
Bob

2
@Bob이 경우 0에 도달하고 결과를 인쇄 한 다음 추가로 줄인 다음 0 아래로 내려 가서 캐리 (또는 빌림)를 유발할 수 있습니다. 그러나 약간 다르게 작성된 루프는 0 플래그를 대신 사용할 수 있습니다.
sigfpe

1
완벽하게 pedantic하기 위해 모든 현대 하드웨어가 파이프 라인되는 것은 아닙니다. 임베디드 프로세서는 이러한 종류의 마이크로 최적화와 훨씬 관련이 있습니다.
Paul Nathan

@Paul Atmel AVR에 대한 경험이
있으므로

27

Intel x86 명령어 세트에서 0으로 카운트 다운하는 루프를 구축하는 것은 일반적으로 0이 아닌 종료 조건까지 카운트하는 루프보다 적은 명령어로 수행 할 수 있습니다. 특히 ECX 레지스터는 전통적으로 x86 asm에서 루프 카운터로 사용되며 인텔 명령어 세트에는 ECX 레지스터를 0으로 테스트하고 테스트 결과에 따라 점프하는 특수 jcxz 점프 명령이 있습니다.

그러나 루프가 이미 클럭 사이클 수에 매우 민감하지 않으면 성능 차이는 무시할 수 있습니다. 카운트 다운을 0으로 카운트 다운하면 카운트 업에 비해 루프의 각 반복에서 4 또는 5 클럭 사이클을 줄일 수 있으므로 유용한 기술보다 참신합니다.

또한 요즘 잘 최적화 된 컴파일러는 루프 업 변수를 사용하는 방법에 따라 카운트 업 루프 소스 코드를 0의 기계 코드로 카운트 다운으로 변환 할 수 있어야하므로 실제로 루프를 작성할 이유가 없습니다. 여기저기서주기를 짜는 이상한 방법.


2
몇 년 전부터 Microsoft의 C ++ 컴파일러가 최적화를하는 것을 보았습니다. 루프 인덱스가 사용되지 않음을 알 수 있으므로 가장 빠른 형식으로 다시 정렬합니다.
마크 랜섬

1
@Mark : 1996 년부터 시작되는 델파이 컴파일러.
dthorpe

4
@MarkRansom 실제로, 루프 인덱스 변수가 루프에서 사용되는 방식에 따라 루프 인덱스 변수를 사용하더라도 컴파일러는 카운트 다운을 사용하여 루프를 구현할 수 있습니다. 루프 인덱스 변수가 정적 배열 (컴파일 시간에 알려진 크기의 배열)로만 인덱스하는 데만 사용되는 경우 배열 인덱스는 ptr + 배열 크기-루프 인덱스 var로 수행 할 수 있으며 x86에서는 단일 명령 일 수 있습니다. 어셈블러를 디버깅하고 루프 카운트 다운을 보지만 배열 인덱스가 올라가는 것을 보는 것은 매우 열악합니다!
dthorpe

1
실제로 오늘날 컴파일러는 dec 및 jnz 쌍보다 느린 루프 및 jecxz 명령어를 사용하지 않을 것입니다.
fuz

1
@FUZxxl 루프를 이상한 방식으로 쓰지 않는 더 많은 이유. 사람이 읽을 수있는 명확한 코드를 작성하고 컴파일러가 작업을 수행하게하십시오.
dthorpe

23

예..!!

하드웨어에서 비교를 처리하는 방법의 관점에서 N에서 0으로 계산하는 것이 0에서 N으로 계산하는 것보다 약간 빠릅니다.

각 루프 의 비교 에 유의하십시오

i>=0
i<N

대부분의 프로세서는 제로 명령어와 비교됩니다. 따라서 첫 번째 프로세서는 다음과 같이 기계어 코드로 변환됩니다.

  1. 내가로드
  2. 0보다 작거나 같은 경우 비교 및 ​​점프

그러나 두 번째는 매번 N 양식 메모리를로드해야합니다.

  1. 내가로드
  2. 부하 N
  3. Sub i와 N
  4. 0보다 작거나 같은 경우 비교 및 ​​점프

카운트 다운이나 카운트 업이 아니라 코드가 기계 코드로 변환되는 방식 때문입니다.

따라서 10에서 100까지의 계수는 100에서 10까지의 계수와 동일
하지만 i = 100에서 0까지의 계수는 i = 0에서 100보다 빠릅니다. 대부분의 경우
i = N에서 0까지의 계수는 i =보다 빠릅니다. 0에서 N

  • 오늘날 컴파일러는 당신을 위해이 최적화를 수행 할 수 있습니다 (충분히 똑똑하다면)
  • 파이프 라인은 Belady의 변칙적 인 효과를 유발할 수 있습니다 (더 나은 것이 무엇인지 확신 할 수 없음)
  • 마지막으로, 제시 한 2 개의 for 루프는 동일하지 않습니다. 첫 번째는 하나 더 * * 인쇄합니다.

관련 : 왜 n ++이 n = n + 1보다 빠르게 실행됩니까?


6
그래서 당신이 말하는 것은 카운트 다운하는 것이 빠르지 않고 다른 어떤 값보다 0과 비교하는 것이 더 빠르다는 것입니다. 10에서 100까지 카운트하는 것과 100에서 10까지 카운트 다운하는 것은 동일합니까?
Bob

8
예 .. "카운트 다운 또는 업"문제가 아니라 "무엇과 비교"문제입니다.
Betamoo

3
이것이 사실이지만 어셈블러 수준입니다. 긴 파이프를 사용하는 현대식 하드웨어와 추론 적 명령이 추가 사이클을 발생시키지 않고 "Sub i 및 N"에서 몰래 빠져 나가는 두 가지가 현실에 맞지 않습니다. N "이 없습니다.
제임스 앤더슨

2
@nico 고대 시스템 일 필요는 없습니다. 레지스터 값과 동등한 비교보다 어떤 식 으로든 더 빠르거나 더 나은 제로 연산과 비교되는 명령 세트 일뿐입니다. x86은 jcxz에 있습니다. x64에는 여전히 있습니다. 고대가 아닙니다. 또한 RISC 아키텍처는 종종 특수한 경우 0입니다. 예를 들어, DEC AXP Alpha 칩 (MIPS 제품군)에는 "제로 레지스터"가있었습니다. 0으로 읽으면 쓰기가 아무 것도 수행하지 않습니다. 제로 값을 포함하는 일반 레지스터와 비교하여 제로 레지스터와 비교하면 명령 간 종속성이 줄어들고 순서가 잘못 실행되는 데 도움이됩니다.
dthorpe

5
@Betamoo : 나는 더 나은 / 더 정확한 답변 (귀하의 답변)이 더 많은 투표로 더 높이 평가되지 않는 이유에 대해 종종 궁금해하고 있습니다. 답변이 정확하지 않음
Artur

12

C에서 pseudo-assembly로 :

for (i = 0; i < 10; i++) {
    foo(i);
}

로 변하다

    clear i
top_of_loop:
    call foo
    increment i
    compare 10, i
    jump_less top_of_loop

동안:

for (i = 10; i >= 0; i--) {
    foo(i);
}

로 변하다

    load i, 10
top_of_loop:
    call foo
    decrement i
    jump_not_neg top_of_loop

두 번째 의사 조립에서는 비교가 부족합니다. 많은 아키텍처에는 점프에 사용할 수있는 산술 연산 (더하기, 빼기, 곱하기, 나누기, 증분, 감소)으로 설정된 플래그가 있습니다. 이것들은 종종 오퍼레이션 결과와 0을 무료로 비교하는 것을 종종 제공합니다. 실제로 많은 아키텍처에서

x = x - 0

의미 상으로

compare x, 0

또한 내 예제에서 10을 비교하면 코드가 더 나빠질 수 있습니다. 10은 레지스터에 있어야 할 수도 있으므로, 공급이 부족한 경우 루프를 통해 매번 10을 돌아 다니거나 다시로드하는 추가 코드가 발생할 수 있습니다.

컴파일러는 때때로 이것을 이용하기 위해 코드를 재 배열 할 수 있지만, 루프를 통한 방향 반전이 의미 상 동등하다는 것을 확신 할 수 없기 때문에 종종 어렵다.


1이 아닌 2 개의 명령어가 다른 것이 가능합니까?
Pacerier

또한 왜 그렇게 확신하기 어렵습니까? var i이 루프 내에서 사용되지 않는 한 분명히 뒤집을 수 있습니까?
Pacerier

6

다음과 같은 경우 더 빨리 카운트 다운하십시오.

for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}

someObject.getAllObjects.size()처음에 한 번 실행 되기 때문 입니다.


물론 size()베드로가 언급했듯이 비슷한 행동을 반복 할 수 있습니다 .

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}

5
"확실히 빠르지"않다. 많은 경우에 count () 호출은 카운트 업할 때 루프에서 벗어날 수 있으므로 여전히 한 번만 호출됩니다. 분명히 이것은 언어 및 컴파일러에 따라 다릅니다 (예 : 코드에 따라 다릅니다 (예 : C ++에서는 size ()가 가상이면 게양되지 않습니다)). 그러나 어느 쪽도 명확하지 않습니다.
Peter

3
@Peter : 컴파일러가 size ()가 루프 전체에서 dem 등하다는 것을 알고있는 경우에만. 루프가 매우 단순 하지 않으면 거의 항상 그렇지 않습니다 .
Lawrence Dol

@LawrenceDol, 당신이 사용하는 동적 코드 compilatino가 없다면 컴파일러는 그것을 확실히 알 것 exec입니다.
Pacerier

4

위로 카운트 다운하는 것이 더 빠릅니까?

아마도. 그러나 시간의 99 % 이상이 중요하지 않으므로 루프를 종료하기 위해 가장 '현상적인'테스트를 사용해야하며 합리적인 경우 독자가 생각하는 데 가장 적은 양의 생각이 필요하다는 것을 의미합니다. 루프가 수행하는 작업 (중지되는 작업 포함) 코드가 수행하는 작업의 정신적 (또는 문서화 된) 모델과 일치하도록하십시오.

루프가 배열 (또는 목록 또는 기타)을 통해 작동하는 경우 증가하는 카운터는 종종 독자가 루프가하는 일을 생각하는 방식과 더 잘 일치합니다-루프를 이런 식으로 코딩하십시오.

그러나 N항목 이있는 컨테이너를 통해 작업 하면서 항목을 제거하는 경우 카운터를 아래로 내리는 것이 더인지적일 수 있습니다.

대답의 '아마도'에 대해 좀 더 자세히 설명하십시오.

대부분의 아키텍처에서 계산 결과를 0 (또는 0에서 음으로 변경)으로 테스트 할 때는 명시적인 테스트 명령이 필요하지 않으며 결과를 직접 확인할 수 있습니다. 계산 결과 다른 숫자가 나오는지 테스트하려는 경우 명령 스트림에는 일반적으로 해당 값을 테스트하기위한 명시적인 명령이 있어야합니다. 그러나 특히 최신 CPU의 경우이 테스트는 일반적으로 루프 구성에 노이즈 수준보다 적은 시간을 더 추가합니다. 특히 해당 루프가 I / O를 수행하는 경우.

반면에 0에서 카운트 다운하고 카운터를 배열 인덱스로 사용하면 시스템의 메모리 아키텍처에 대해 작동하는 코드를 찾을 수 있습니다. 메모리 읽기는 종종 캐시를 '예상'하게합니다 순차 읽기를 예상하여 현재 메모리를 지나는 여러 메모리 위치. 메모리를 통해 거꾸로 작업하는 경우 캐싱 시스템은 낮은 메모리 주소에서 메모리 위치를 읽지 못할 수 있습니다. 이 경우 '뒤로'반복하면 성능이 저하 될 수 있습니다. 그러나 정확성이 가장 중요하기 때문에 성능을 문제가되지 않는 한 여전히 루프를 이런 식으로 코딩하고 코드를 모델과 일치시키는 것이 정확성을 보장하는 좋은 방법입니다. 잘못된 코드는 가능한 한 최적화되지 않았습니다.

따라서 코드의 성능이 실제로 문제가되지 않는 한 교수진의 조언을 잊는 경향이 있습니다.


3

일부 구형 CPU에는 DJNZ== "0이 아닌 경우 감소 및 점프" 와 같은 명령이있었습니다 . 이를 통해 초기 카운트 값을 레지스터에로드 한 효율적인 루프가 가능하며 하나의 명령으로 감소 루프를 효과적으로 관리 할 수 ​​있습니다. 우리는 여기서 1980 년대 ISA를 이야기하고 있습니다. 선생님이이 "엄지 규칙"이 여전히 현대 CPU에 적용된다고 생각한다면, 당신의 교사는 진지하게 연락이 없습니다.


3

단발,

미세 최적화를 수행하기 전까지는 CPU에 대한 매뉴얼이 제공됩니다. 또한, 당신이 그런 종류의 일을하고 있다면 어쨌든이 질문을 할 필요가 없을 것입니다. :-) 그러나 선생님은 분명히 그 아이디어를 구독하지 않습니다 ....

루프 예제에서 고려해야 할 4 가지가 있습니다.

for (i=N; 
 i>=0;             //thing 1
 i--)             //thing 2
{
  putchar('*');   //thing 3
}
  • 비교

비교는 (다른 사람들이 지적했듯이) 특정 프로세서 아키텍처 와 관련이 있습니다 . Windows를 실행하는 것보다 많은 유형의 프로세서가 있습니다. 특히, 0과의 비교를 단순화하고 속도를 높이는 명령이있을 수 있습니다.

  • 조정

경우에 따라 위 또는 아래로 조정하는 것이 더 빠릅니다. 일반적으로 좋은 컴파일러는 알아 내고 가능하면 루프를 다시 실행합니다. 모든 컴파일러가 좋은 것은 아닙니다.

  • 루프 바디

putchar로 syscall에 액세스하고 있습니다. 엄청 느립니다. 또한 화면에 간접적으로 렌더링됩니다. 심지어 더 느립니다. 1000 : 1 이상의 비율을 생각하십시오. 이 상황에서 루프 본체는 전체적으로 루프 조정 / 비교 비용을 능가합니다.

  • 캐시

캐시 및 메모리 레이아웃은 성능에 큰 영향을 줄 수 있습니다. 이 상황에서는 중요하지 않습니다. 그러나 어레이에 액세스하고 최적의 성능이 필요한 경우 컴파일러와 프로세서가 메모리 액세스를 어떻게 배치했는지 조사하고 소프트웨어를 최대한 활용하도록 튜닝해야합니다. 스톡 예제는 행렬 곱셈과 관련하여 주어진 것입니다.


3

카운터를 늘리거나 줄이는 것보다 훨씬 중요한 것은 메모리를 늘리거나 내리는 것입니다. 대부분의 캐시는 메모리가 아닌 메모리를 늘리는 데 최적화되어 있습니다. 메모리 액세스 시간은 오늘날 대부분의 프로그램이 직면하는 병목 현상이므로 카운터를 0이 아닌 값과 비교해야하는 경우에도 메모리를 늘리도록 프로그램을 변경하면 성능이 향상 될 수 있습니다. 일부 프로그램에서 코드를 변경하지 않고 메모리를 늘리도록 코드를 변경하여 성능이 크게 향상되었습니다.

의심 많은? 메모리를 올리거나 내리는 시간 루프에 프로그램을 작성하십시오. 내가 얻은 결과는 다음과 같습니다.

Average Up Memory   = 4839 mus
Average Down Memory = 5552 mus

Average Up Memory   = 18638 mus
Average Down Memory = 19053 mus

( "mus"는 마이크로 초를 나타냄)이 프로그램을 실행하는 것 :

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//Sum all numbers going up memory.
template<class Iterator, class T>
inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

//Sum all numbers going down memory.
template<class Iterator, class T>
inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

//Time how long it takes to make num_repititions identical calls to sum_abs_down().
//We will divide this time by num_repitions to get the average time.
template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &vec_original,
                                  std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &vec_original,
                                std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(vec_size);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up   = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "Average Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Average Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

모두 sum_abs_upsum_abs_down같은 일을 (숫자의 벡터 합계) 할 유일한 차이 존재와 같은 방식으로 시간이 초과되는 sum_abs_up동안 메모리를 간다 sum_abs_down메모리 아래로 이동합니다. vec두 함수가 동일한 메모리 위치에 액세스하도록 참조로 전달 합니다. 그럼에도 불구하고 sum_abs_up지속적으로보다 빠릅니다 sum_abs_down. 직접 실행하십시오 (g ++ -O3으로 컴파일했습니다).

내가 타이밍을 맞추고있는 루프가 얼마나 타이트한 지 주목하는 것이 중요하다. 루프 본문이 크면 루프 본문을 실행하는 데 걸리는 시간이 완전히 지배되므로 반복자가 메모리를 늘리거나 내리는 지 여부는 중요하지 않습니다. 또한 드문 루프를 사용하면 메모리를 낮추는 것이 때로는 올라가는 것보다 빠르다 는 점도 언급 해야합니다. 그러나 이러한 루프를 사용하더라도 메모리를 올리는 것보다 항상 메모리를 올리는 것이 느리게 진행되는 경우는 결코 없었 습니다. 예를 들어, 메모리를 늘리면 성능이 40 % 이상 증가했습니다.

요컨대, 선택의 여지가 있다면 루프 몸체가 작고 루프가 메모리가 아닌 메모리 위로 올라가는 것과 차이가 거의 없다면 메모리를 위로 올라 가야한다는 것입니다.

참고로 vec_original변화에 쉽게 그것을 만들기 위해 실험을 위해 존재 sum_abs_up하고 sum_abs_down그들을 변경할 수있는 방법으로 vec이러한 변화는 미래의 타이밍에 영향을 허용하지 않는 동안. 내가보기 엔 놀아 추천 sum_abs_up하고 sum_abs_down그 결과 타이밍.


2

방향에 관계없이 항상 접두사 양식 (i ++ 대신 ++ i)을 사용하십시오!

for (i=N; i>=0; --i)  

또는

for (i=0; i<N; ++i) 

설명 : http://www.eskimo.com/~scs/cclass/notes/sx7b.html

또한 당신은 쓸 수 있습니다

for (i=N; i; --i)  

그러나 현대 컴파일러가 이러한 최적화를 정확하게 수행 할 수있을 것으로 기대합니다.


사람들이 전에 그것에 대해 불평하는 것을 본 적이 없습니다. 그러나 링크를 읽은 후에 실제로 의미가 있습니다. :) 감사합니다.
Tommy Jakobsen

3
왜 항상 접두사 형식을 사용해야합니까? 과제가없는 경우에도 동일하며 링크 된 기사에서도 접미사 형식이 더 일반적이라고 말합니다.
bobDevil

3
왜 항상 접두사 양식을 사용해야합니까? 이 경우 의미 적으로 동일합니다.
Ben Zotto

2
접미사 형식은 잠재적으로 불필요한 객체 복사본을 만들 수 있지만 값을 사용하지 않으면 컴파일러는 접두사 형식으로 최적화합니다.
Nick Lewis

C 컴퓨터를 배웠을 때 일반적으로 레지스터 사전 감소 및 사후 증가가 있었지만 그 반대는 아니기 때문에 습관이 없어서 항상 --i와 i ++를 사용합니다. 따라서 * p ++ 및 *-p는 * ++ p 및 * p보다 빠릅니다. 이전 두 명령은 하나의 68000 기계 코드 명령으로 수행 될 수 있기 때문입니다.
JeremyP

2

흥미로운 질문이지만 실제적인 문제는 중요하지 않다고 생각하고 하나의 루프를 다른 루프보다 나은 것으로 만들지 않습니다.

이 위키 백과 페이지에 따르면 , 윤초 (Leap second )는 "... 일조 는 주로 조력 마찰로 인해 세기마다 1.7ms 더 길어진다"고 말했다. 그러나 생일까지 며칠을 세고 있다면 시간의 작은 차이에 정말로 관심이 있습니까?

소스 코드를 읽고 이해하기가 더 중요합니다. 이 두 루프는 왜 가독성이 중요한지에 대한 좋은 예입니다. 동일한 횟수만큼 반복되지 않습니다.

나는 대부분의 프로그래머가 (i = 0; i <N; i ++)를 읽고 이것이 N 번 반복된다는 것을 즉시 이해한다고 확신합니다. 어쨌든 (i = 1; i <= N; i ++)의 루프는 조금 덜 명확하고 (i = N; i> 0; i--)로 잠시 생각해야합니다. . 코드의 의도가 생각없이 뇌로 직접 들어가는 것이 가장 좋습니다.


두 구조는 모두 이해하기 쉽습니다. 반복 횟수가 3 ~ 4 회인 경우 루프를 만드는 것보다 이해하기 쉽기 때문에 명령을 복사하는 것이 좋습니다.
Danubian Sailor

2

이상하게도 차이가있는 것으로 보입니다. 적어도 PHP에서는. 다음 벤치 마크를 고려하십시오.

<?php

print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;

$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);

$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);

흥미로운 결과 :

PHP 5.2.13
$i++ : 8.8842368125916
$i-- : 8.1797409057617
++$i : 8.0271911621094
--$i : 7.1027431488037


PHP 5.3.1
$i++ : 8.9625310897827
$i-- : 8.5790238380432
++$i : 5.9647901058197
--$i : 5.4021768569946

누군가 이유를 안다면, 알아두면 좋을 것입니다 :)

편집 : 0부터 시작하지 않고 다른 임의의 값을 계산하더라도 결과는 동일합니다. 그렇다면 아마도 0과 비교하여 차이를 만드는 것은 아닐까요?


속도가 느린 이유는 접두사 연산자가 임시를 저장할 필요가 없기 때문입니다. $ foo = $ i ++; 세 가지 일이 발생합니다. $ i는 임시에 저장되고 $ i는 증가한 다음 $ foo에 해당 임시 값이 할당됩니다. $ i ++의 경우; 똑똑한 컴파일러는 임시가 불필요하다는 것을 알 수 있습니다. PHP는 그렇지 않습니다. C ++ 및 Java 컴파일러는이 간단한 최적화를 수행 할 수있을 정도로 똑똑합니다.
눈에 띄는 컴파일러

왜 $ i--가 $ i ++보다 더 빠릅니까?
ts.

얼마나 많은 벤치 마크 반복을 실행 했습니까? 아웃 라이더를 클립하고 각 결과에 대해 평균을 얻었습니까? 벤치 마크 중에 컴퓨터가 다른 작업을 수행 했습니까? ~ 0.5의 차이는 다른 CPU 활동이나 파이프 라인 활용의 결과 일 수 있습니다.
8 비트 전문가

예, 여기에 평균을 제공합니다. 벤치 마크는 다른 머신에서 실행되었으며 우연히 차이가 있습니다.
ts.

@Conspicuous Compiler => 알고 계십니까?
ts.

2

그것은 빠릅니다.

현재 작업중인 NIOS II 프로세서에서 전통적인 for 루프

for(i=0;i<100;i++)

어셈블리를 생성합니다.

ldw r2,-3340(fp) %load i to r2
addi r2,r2,1     %increase i by 1
stw r2,-3340(fp) %save value of i
ldw r2,-3340(fp) %load value again (???)
cmplti r2,r2,100 %compare if less than equal 100
bne r2,zero,0xa018 %jump

카운트 다운하면

for(i=100;i--;)

2 개의 명령이 덜 필요한 어셈블리를 얻습니다.

ldw r2,-3340(fp)
addi r3,r2,-1
stw r3,-3340(fp)
bne r2,zero,0xa01c

내부 루프가 많이 실행되는 중첩 루프가있는 경우 측정 가능한 차이가 있습니다.

int i,j,a=0;
for(i=100;i--;){
    for(j=10000;j--;){
        a = j+1;
    }
}

내부 루프가 위와 같이 작성된 경우 실행 시간은 0.12199999999999999734 초입니다. 내부 루프가 전통적인 방식으로 작성된 경우 실행 시간은 0.17199999999999998623 초입니다. 따라서 루프 카운트 다운은 약 30 % 빠릅니다.

그러나이 테스트는 모든 GCC 최적화가 해제 된 상태에서 수행되었습니다. 그것들을 켜면 컴파일러는 실제로이 손쉬운 최적화보다 똑똑하고 전체 루프 동안 레지스터에 값을 유지하며 다음과 같은 어셈블리를 얻습니다

addi r2,r2,-1
bne r2,zero,0xa01c

이 특정 예제에서 컴파일러 는 루프 실행 후 변수 a 가 항상 1이고 루프를 모두 건너 뜁니다.

그러나 때로는 루프 본문이 충분히 복잡하면 컴파일러 가이 최적화를 수행 할 수 없으므로 항상 빠른 루프 실행을 얻는 가장 안전한 방법은 다음과 같습니다.

register int i;
for(i=10000;i--;)
{ ... }

물론 이것은 루프가 반대로 실행되고 Betamoo가 말한 것처럼 0으로 카운트 다운하는 경우에만 작동 합니다.


2

선생님이 말씀하신 내용은 명확하지 않은 약간의 진술입니다. 감소가 증분보다 빠르지는 않지만 증분보다 감소로 훨씬 빠른 루프를 만들 수 있습니다.

루프 카운터 등을 사용할 필요없이 길이에 관계없이 속도와 루프 수 (0이 아님)가 중요합니다.

대부분의 사람들이 10 회 반복하여 루프를 구현하는 방법은 다음과 같습니다.

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

99 %의 경우 모두 필요하지만 PHP, PYTHON, JavaScript와 함께 CPU 틱이 정말로 중요한 세계적으로 중요한 소프트웨어 (일반적으로 임베디드, OS, 게임 등)가 있으므로 어셈블리 코드를 간단히 살펴보십시오.

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

컴파일 후 (최적화없이) 컴파일 된 버전은 다음과 같습니다 (VS2015).

-------- C7 45 B0 00 00 00 00  mov         dword ptr [i],0  
-------- EB 09                 jmp         labelB 
labelA   8B 45 B0              mov         eax,dword ptr [i]  
-------- 83 C0 01              add         eax,1  
-------- 89 45 B0              mov         dword ptr [i],eax  
labelB   83 7D B0 0A           cmp         dword ptr [i],0Ah  
-------- 7D 02                 jge         out1 
-------- EB EF                 jmp         labelA  
out1:

전체 루프는 8 개의 명령어 (26 바이트)입니다. 그 안에는 실제로 2 개의 분기가있는 6 개의 명령어 (17 바이트)가 있습니다. 예, 나는 그것이 더 잘 할 수 있다는 것을 알고 있습니다 (그냥 예일뿐입니다).

이제 임베디드 개발자가 작성하는 빈번한 구성을 고려하십시오.

i = 10;
do
{
    //something here
} while (--i);

또한 10 번 반복합니다 (예 : for 루프와 비교하여 i 값이 다르다는 것을 알고 있지만 반복 횟수는 여기서 중요합니다). 이것은 이것으로 컴파일 될 수 있습니다 :

00074EBC C7 45 B0 01 00 00 00 mov         dword ptr [i],1  
00074EC3 8B 45 B0             mov         eax,dword ptr [i]  
00074EC6 83 E8 01             sub         eax,1  
00074EC9 89 45 B0             mov         dword ptr [i],eax  
00074ECC 75 F5                jne         main+0C3h (074EC3h)  

5 개의 명령어 (18 바이트)와 단 하나의 브랜치 실제로 루프에는 4 개의 명령어가 있습니다 (11 바이트).

가장 좋은 점은 일부 CPU (x86 / x64 호환 포함)에 레지스터를 감소시킬 수있는 명령이 있고, 결과를 0과 비교하고 결과가 0과 다른 경우 분기를 수행하는 것입니다. 거의 모든 PC CPU가이 명령어를 구현합니다. 그것을 사용하면 루프는 실제로 하나의 (예 하나) 2 바이트 명령입니다.

00144ECE B9 0A 00 00 00       mov         ecx,0Ah  
label:
                          // something here
00144ED3 E2 FE                loop        label (0144ED3h)  // decrement ecx and jump to label if not zero

어느 것이 더 빠른지 설명해야합니까?

이제 특정 CPU가 위의 명령을 구현하지 않더라도 이전 명령의 결과가 0이면 조건을 뛰어 넘고 감소시키는 에뮬레이션에 필요한 모든 것입니다.

따라서 어떤 경우에 관계없이 내가 왜 잘못되었는지 등의 의견으로 지적 할 수 있습니다.

추신. 예, 현명한 컴파일러 (적절한 최적화 수준의)는 루프 (오름차순 루프 카운터 사용)를 위해 루프를 다시 작성한다는 것을 알고 있습니다.


1

아니요, 사실이 아닙니다. 더 빠를 수있는 상황 중 하나는 루프를 반복 할 때마다 경계를 확인하기 위해 함수를 호출하는 경우입니다.

for(int i=myCollection.size(); i >= 0; i--)
{
   ...
}

그러나 그렇게하는 것이 명확하지 않으면 가치가 없습니다. 현대 언어에서는 가능하면 foreach 루프를 사용해야합니다. 인덱스가 필요 없을 때 foreach 루프를 사용해야하는 경우를 구체적으로 언급합니다.


1
명확 하고 효율적이 려면 최소한 습관이 있어야 for(int i=0, siz=myCollection.size(); i<siz; i++)합니다.
Lawrence Dol

1

요점은 카운트 다운 할 때 i >= 0감소 시키기 위해 별도로 확인할 필요가 없다는 것 i입니다. 관찰 :

for (i = 5; i--;) {
  alert(i);  // alert boxes showing 4, 3, 2, 1, 0
}

비교와 감소 i는 하나의 표현으로 수행 할 수 있습니다.

x86 명령어 수가 더 적은 이유에 대한 다른 답변을 참조하십시오.

그것이 응용 프로그램에서 의미있는 차이를 만드는지 여부에 관해서는, 얼마나 많은 루프가 있고 얼마나 중첩되어 있는지에 달려 있습니다. 그러나 나에게 이런 식으로 읽을 수있는 것처럼 읽을 수 있으므로 어쨌든합니다.


나는 이것이 나쁜 스타일이라고 생각합니다. 왜냐하면 그것은 사이클을 절약 할 수있는 가치에 대해 i의 반환 값이 i의 오래된 값이라는 것을 아는 독자에 달려 있기 때문입니다. 루프 반복이 많은 경우에만 중요하며주기는 반복 길이의 상당 부분이며 실제로 런타임에 나타났습니다. 다음으로 누군가가 (i = 5; --i;) 시도 할 것입니다. 왜냐하면 C ++에서는 내가 사소한 유형 일 때 임시 파일을 작성하지 않으려 고 할 수 있다고 들었으므로 이제는 버그가 있습니다. 실수로 잘못된 코드를 잘못 표시 할 수있는 기회를 버리십시오.
mabraham

0

이제, 당신은 충분한 어셈블리 강의를했다고 생각합니다 :) 나는 당신에게 top-> down 접근의 또 다른 이유를 제시하고 싶습니다.

정상에서 출발하는 이유는 매우 간단합니다. 루프 본문에서 실수로 경계를 변경하여 잘못된 동작 또는 종료되지 않은 루프로 끝날 수 있습니다.

Java 코드의이 작은 부분을보십시오 (이 이유 때문에 언어는 중요하지 않습니다).

    System.out.println("top->down");
    int n = 999;
    for (int i = n; i >= 0; i--) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }
    System.out.println("bottom->up");
    n = 1;
    for (int i = 0; i < n; i++) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }

내 요점은 위에서 아래로가는 것을 선호하거나 경계로 상수를 갖는 것을 고려해야한다는 것입니다.


응? !! 당신이 실패한 예는 실제로 반 직관적입니다. 즉, 밀짚 꾼 논쟁 – 아무도 이것을 쓰지 않을 것입니다. 하나는 쓸 것이다 for (int i=0; i < 999; i++) {.
Lawrence Dol

@Software Monkey는 n이 계산의 결과라고 상상합니다. 예를 들어 일부 컬렉션을 반복하고 크기는 경계이지만 부작용으로 인해 루프 본문의 컬렉션에 새로운 요소를 추가 할 수 있습니다.
Gabriel Ščerbák 8:13에

그것이 당신이 의사 소통하려는 의도라면, 그것은 당신의 모범이 설명해야 할 것입니다.for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Lawrence Dol

@Software Monkey 저는 단지 컬렉션에 대해 이야기하는 것보다 더 일반적이기를 원했습니다. 왜냐하면 내가 추론하는 것은 컬렉션과 아무 관련이 없기 때문입니다.
Gabriel Ščerbák

2
그렇습니다. 그러나 예를 들어 추론을한다면, 그 예는 신뢰할 만하고 요점을 잘 설명해야합니다.
Lawrence Dol

-1

어셈블러 수준에서 0으로 카운트 다운하는 루프는 일반적으로 주어진 값까지 카운트되는 루프보다 약간 빠릅니다. 계산 결과가 0이면 대부분의 프로세서는 0 플래그를 설정합니다. 하나를 빼면 0을지나 계산 랩을 만들면 일반적으로 캐리 플래그가 변경됩니다 (일부 프로세서에서는 다른 플래그를 설정합니다) .0과의 비교는 본질적으로 무료입니다.

반복 횟수가 상수가 아니라 변수 인 경우 더욱 그렇습니다.

사소한 경우에는 컴파일러가 루프의 카운트 방향을 자동으로 최적화 할 수 있지만 더 복잡한 경우에는 루프의 방향이 전체 동작과 관련이 없음을 알고 있지만 컴파일러는이를 입증 할 수 없습니다.

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