루프 언 롤링이 언제 여전히 유용합니까?


93

루프 언 롤링을 통해 성능에 매우 중요한 코드 (몬테카를로 시뮬레이션 내에서 수백만 번 호출되는 빠른 정렬 알고리즘)를 최적화하려고했습니다. 속도를 높이려는 내부 루프는 다음과 같습니다.

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

나는 다음과 같이 풀어 보았습니다.

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

이것은 전혀 차이가 없었기 때문에 더 읽기 쉬운 형식으로 다시 변경했습니다. 나는 루프 언 롤링을 시도한 다른 시간에도 비슷한 경험을했습니다. 현대 하드웨어에서 분기 예측 자의 품질을 고려할 때 루프 언 롤링이 여전히 유용한 최적화일까요?


1
표준 라이브러리 퀵 정렬 루틴을 사용하지 않는 이유를 물어봐도 될까요?
Peter Alexander

14
@Poita : 내 통계 계산에 필요한 몇 가지 추가 기능이 있고 내 사용 사례에 맞게 매우 조정되어 있으므로 덜 일반적이지만 표준 라이브러리보다 훨씬 빠릅니다. 나는 오래된 엉뚱한 최적화 프로그램이있는 D 프로그래밍 언어를 사용하고 있으며, 임의의 큰 배열의 경우 여전히 GCC의 C ++ STL 정렬을 10-20 % 능가합니다.
dsimcha

답변:


122

루프 언 롤링은 종속성 체인을 끊을 수 있다면 의미가 있습니다. 이것은 순서가 맞지 않거나 슈퍼 스칼라 CPU에 일을 더 잘 예약하여 더 빨리 실행할 수있는 가능성을 제공합니다.

간단한 예 :

for (int i=0; i<n; i++)
{
  sum += data[i];
}

여기서 인수의 종속성 체인은 매우 짧습니다. 데이터 어레이에 캐시 미스가있어 지연이 발생하면 CPU는 대기하는 것 외에는 아무것도 할 수 없습니다.

반면에이 코드 :

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

더 빨리 달릴 수 있습니다. 하나의 계산에서 캐시 미스 또는 기타 지연이 발생하는 경우 지연에 의존하지 않는 다른 종속성 체인이 세 개 있습니다. 고장난 CPU가이를 실행할 수 있습니다.


2
감사. 나는 합계와 물건을 계산하는 라이브러리의 다른 여러 곳 에서이 스타일로 루프 풀기를 시도했으며 이러한 곳에서는 놀라운 일을합니다. 그 이유는 당신이 제안한 것처럼 명령 수준의 병렬 처리를 증가시키기 때문이라고 거의 확신합니다.
dsimcha

2
좋은 대답과 유익한 예. 캐시 미스에 대한 지연 이이 특정 예제의 성능 에 어떤 영향을 미칠 수 있는지는 알 수 없습니다 . 첫 번째 코드가 부동 소수점 레인에서 모든 종류의 명령 수준 병렬 처리를 비활성화한다는 점에 주목하여 두 코드 조각 (내 컴퓨터에서 두 번째 코드가 2-3 배 빠름) 간의 성능 차이를 설명하려고했습니다. 두 번째는 슈퍼 스칼라 CPU가 최대 4 개의 부동 소수점 추가를 동시에 실행할 수 있도록합니다.
Toby Brull 2014

2
이런 식으로 합계를 계산할 때 결과는 원래 루프와 수치 적으로 동일하지 않습니다.
Barabas

루프 수행 종속성은 하나의주기 , 추가입니다. OoO 코어는 괜찮습니다. 여기서 언 롤링은 부동 소수점 SIMD에 도움이 될 수 있지만 OoO에 관한 것이 아닙니다.
Veedrac

2
@Nils : 그다지 많지 않습니다. 주류 x86 OoO CPU는 여전히 Core2 / Nehalem / K10과 유사합니다. 캐시 미스 후 따라 잡는 것은 여전히 ​​매우 사소한 일 이었지만 FP 대기 시간을 숨기는 것이 여전히 주요 이점이었습니다. 2010 년에는 클럭 당 2 개의로드를 수행 할 수있는 CPU가 훨씬 더 드물었 기 때문에 (SnB가 아직 출시되지 않았기 때문에 AMD 만 해당) 다중 누산기는 지금보다 정수 코드에 대해 확실히 덜 가치가있었습니다 (물론 이것은 자동 벡터화해야하는 스칼라 코드입니다. , 그래서 컴파일러가 여러 누산기를 벡터 요소로 변환할지 아니면 여러 벡터 누산기
겠습니까

25

동일한 수의 비교를 수행하고 있기 때문에 차이가 없습니다. 더 나은 예가 있습니다. 대신에:

for (int i=0; i<200; i++) {
  doStuff();
}

쓰다:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

그럼에도 불구하고 거의 확실하게 중요하지 않지만 이제 200 개 대신 50 개의 비교를 수행하고 있습니다 (비교가 더 복잡하다고 상상해보십시오).

그러나 일반적으로 수동 루프 언 롤링은 대부분 역사의 산물입니다. 그것은 중요한 컴파일러가 당신을 위해 해줄 것입니다. 예를 들어, 대부분의 사람들은 쓰고 귀찮게하지 않습니다 x <<= 1또는 x += x대신 x *= 2. 작성 만하면 x *= 2컴파일러가 최선을 다해 최적화합니다.

기본적으로 컴파일러를 추측 할 필요가 점점 줄어 듭니다.


1
@Mike 당황 할 때 좋은 아이디어가 있다면 확실히 최적화를 끄지 만 Poita_가 게시 한 링크를 읽을 가치가 있습니다. 컴파일러는 그 사업에서 고통스럽게 잘하고 있습니다.
dmckee --- ex-moderator kitten

16
@Mike "나는 그 일을 언제, 언제하지 말아야할지 완벽하게 결정할 수있다"... 당신이 초인적이지 않다면 나는 그것을 의심한다.
Mr. Boy

5
@John : 왜 그렇게 말했는지 모르겠습니다. 사람들은 최적화가 일종의 흑인 예술이라고 생각하는 것 같습니다. 그것은 모두 지침과주기, 그리고 그들이 소비되는 이유에 달려 있습니다. 내가 SO에 대해 여러 번 설명했듯이 그것들이 어떻게 그리고 왜 소비되는지 쉽게 알 수 있습니다. 상당한 시간을 사용해야하는 루프가 있고 콘텐츠에 비해 루프 오버 헤드에서 너무 많은 사이클을 소비하면이를 확인하고 풀 수 있습니다. 코드 호이 스팅과 동일합니다. 천재가 필요하지 않습니다.
Mike Dunlavey

3
그렇게 어렵지는 않지만 컴파일러만큼 빨리 할 수 ​​있을지 의심 스럽습니다. 어쨌든 컴파일러가 당신을 위해 그것을하는 문제는 무엇입니까? 마음에 들지 않으면 최적화 기능을 끄고 1990 년처럼 시간을 낭비하십시오!
Mr. Boy

2
루프 언 롤링으로 인한 성능 향상은 저장중인 비교와 관련이 없습니다. 전혀 없습니다.
bobbogo

14

최신 하드웨어의 분기 예측에 관계없이 대부분의 컴파일러는 어쨌든 루프 언 롤링을 수행합니다.

컴파일러가 얼마나 많은 최적화를 수행하는지 알아내는 것은 가치가 있습니다.

나는 Felix von Leitner의 프레젠테이션 이 주제에 대해 매우 깨달음을 얻었습니다. 읽어 보는 것이 좋습니다. 요약 : 최신 컴파일러는 매우 영리하므로 수동 최적화는 거의 효과적이지 않습니다.


7
그것은 좋은 읽기이지만 내가 생각했던 유일한 부분은 그가 데이터 구조를 단순하게 유지하는 것에 대해 이야기하는 곳이었습니다. 어떤 실행되는 것이 있음 - 그것의 나머지는 거대한 무언의 가정에 대한 정확한하지만 달려 있었다 될 수 있습니다. 내가 수행하는 튜닝에서 불필요한 추상화 코드 산더미에 막대한 시간이 소요될 때 레지스터 및 캐시 누락에 대해 걱정하는 사람들을 발견했습니다.
Mike Dunlavey

3
"수작업 최적화는 거의 효과적이지 않습니다."→이 작업에 완전히 익숙하지 않은 경우에는 해당됩니다. 그렇지 않으면 사실이 아닙니다.
Veedrac

2019 년에 나는 여전히 컴파일러의 자동 시도에 비해 상당한 이득이있는 수동 언롤을 수행했습니다. 자주 풀리지 않는 것 같습니다. 적어도 C #의 경우 모든 언어를 대신하여 말할 수는 없습니다.
WDUK

2

내가 이해하는 한, 현대 컴파일러는 이미 적절한 경우 루프를 풀고 있습니다. 예를 들어 gcc가 최적화 플래그를 전달하면 매뉴얼에서 다음과 같이 말합니다.

반복 횟수를 컴파일 타임 또는 루프에 들어갈 때 결정할 수있는 루프를 언롤합니다.

따라서 실제로 컴파일러가 사소한 경우를 수행 할 가능성이 높습니다. 따라서 컴파일러가 필요한 반복 횟수를 결정하기 위해 가능한 많은 루프를 쉽게 확인하는 것은 사용자의 몫입니다.


적시에 컴파일러는 일반적으로 루프 언 롤링을 수행하지 않으며 휴리스틱은 너무 비쌉니다. 정적 컴파일러는 여기에 더 많은 시간을 할애 할 수 있지만 두 가지 주요 방법의 차이가 중요합니다.
Abel

2

수동 언 롤링이든 컴파일러 언 롤링이든 관계없이 루프 언 롤링은 특히 최신 x86 CPU (Core 2, Core i7)에서 역효과를 낼 수 있습니다. 결론 :이 코드를 배포하려는 CPU에서 루프 언 롤링을 사용하거나 사용하지 않고 코드를 벤치마킹하십시오.


왜 특히 recet x86 CPU에서?
JohnTortugo 2013-08-11

7
@JohnTortugo : 최신 x86 CPU에는 작은 루프에 대한 특정 최적화 기능이 있습니다. 예를 들어 Core 및 Nehalem 구조의 Loop Stream Detector 참조-LSD 캐시에 들어갈만큼 충분히 작지 않도록 루프를 풀면이 최적화가 실패합니다. 예 : tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R

1

모르는 사이에 시도하는 것은 그렇게하는 방법이 아닙니다.
이 정렬이 전체 시간에서 높은 비율을 차지합니까?

모든 루프 언 롤링은 증가 / 감소, 중지 조건 비교 및 ​​점프의 루프 오버 헤드를 줄이는 것입니다. 루프에서 수행하는 작업이 루프 오버 헤드 자체보다 더 많은 명령주기를 필요로한다면, 그다지 개선 된 비율을 보지 못할 것입니다.

다음은 최대 성능을 얻는 방법의 예입니다.


1

루프 언 롤링은 특정 경우에 유용 할 수 있습니다. 유일한 이득은 일부 테스트를 건너 뛰는 것이 아닙니다!

예를 들어 스칼라 교체, 소프트웨어 프리 페치의 효율적인 삽입을 허용 할 수 있습니다. 공격적으로 풀면 실제로 얼마나 유용 할 수 있는지 놀라게 될 것입니다 (-O3를 사용해도 대부분의 루프에서 10 % 속도 향상을 쉽게 얻을 수 있음).

앞서 말했듯이 루프에 많이 의존하고 컴파일러와 실험이 필요합니다. 규칙을 만드는 것은 어렵습니다 (또는 언 롤링을위한 컴파일러 휴리스틱이 완벽 할 것입니다).


0

루프 언 롤링은 전적으로 문제 크기에 따라 다릅니다. 크기를 더 작은 작업 그룹으로 줄일 수있는 알고리즘에 전적으로 의존합니다. 위에서 한 것은 그렇게 보이지 않습니다. 몬테카를로 시뮬레이션이 펼쳐질 수 있는지 확실하지 않습니다.

루프 풀기에 대한 좋은 시나리오는 이미지를 회전하는 것입니다. 별도의 작업 그룹을 순환 할 수 있기 때문입니다. 이 작업을 수행하려면 반복 횟수를 줄여야합니다.


시뮬레이션의 메인 루프가 아니라 시뮬레이션의 내부 루프에서 호출되는 빠른 정렬을 풀었습니다.
dsimcha

0

루프 언 롤링은 루프 내부와 루프에 많은 지역 변수가있는 경우 여전히 유용합니다. 루프 인덱스에 대해 하나를 저장하는 대신 해당 레지스터를 더 많이 재사용하려면.

귀하의 예에서는 레지스터를 과도하게 사용하지 않고 소량의 지역 변수를 사용합니다.

비교 (루프 끝까지)는 비교가 무거울 경우 (예 : 비 test명령어) 특히 외부 함수에 의존하는 경우 주요 단점 입니다.

루프 언 롤링은 분기 예측에 대한 CPU의 인식을 높이는데도 도움이되지만 어쨌든 발생합니다.

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