한계가 959이지만 960이 아닌 경우 단순 루프가 최적화되는 이유는 무엇입니까?


131

이 간단한 루프를 고려하십시오.

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

gcc 7 (스냅 샷) 또는 clang (트렁크)을 사용하여 컴파일 -march=core-avx2 -Ofast하면 매우 비슷한 것을 얻을 수 있습니다.

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

즉, 반복하지 않고 답변을 960으로 설정합니다.

그러나 코드를 다음과 같이 변경하면

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

생산 된 어셈블리는 실제로 루프 합계를 수행합니까? 예를 들어 clang은 다음을 제공합니다.

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

왜 이것이 있으며 왜 clang과 gcc에 대해 정확히 동일합니까?


로 교체 float하는 경우 동일한 루프의 한계 double는 479입니다. 이는 gcc 및 clang의 경우에도 동일합니다.

업데이트 1

gcc 7 (스냅 샷)과 clang (트렁크)은 매우 다르게 동작합니다. clang은 내가 말할 수있는 한 960 미만의 모든 한계에 대해 루프를 최적화합니다. 반면에 gcc는 정확한 값에 민감하며 상한이 없습니다. 예를 들어 그것은 않는 한도가 200 인 경우 루프를 최적화 (뿐만 아니라 많은 다른 값)하지만 않는 한도 (202) 및 20002 (뿐만 아니라 많은 다른 값) 인 경우.


3
Sulthan이 의미하는 것은 1) 컴파일러가 루프를 언 롤링하고 2) 언 롤링하면 합 연산이 하나로 그룹화 될 수 있음을 알 수 있습니다. 루프가 풀리지 않으면 작업을 그룹화 할 수 없습니다.
Jean-François Fabre

3
홀수의 루프가 있으면 언 롤링이 더 복잡해 지므로 마지막 몇 번의 반복은 특별히 수행해야합니다. 더 이상 바로 가기를 인식 할 수없는 모드로 최적화 프로그램을 충돌시키기에 충분할 수 있습니다. 아마도 특수한 경우에 대한 코드를 먼저 추가 한 다음 다시 제거해야 할 가능성이 높습니다. 귀 사이에 옵티 마이저를 사용하는 것이 항상 가장 좋습니다 :)
Hans Passant

3
@HansPassant 959보다 작은 숫자에도 최적화되어 있습니다.
eleanora

6
이것은 일반적으로 미친 양을 풀지 않고 유도 변수 제거로 수행되지 않습니까? 959 배만큼 풀리는 것은 미친 짓입니다.
해롤드

4
@eleanora 필자는 해당 컴파일러를 사용하여 다음을 유지하는 것으로 보입니다 (gcc 스냅 샷에 대해서만 이야기). 루프 수가 4의 배수이고 72 이상인 경우 루프가 아닙니다. 풀리지 (또는 오히려 4의 요소; 그렇지 않으면 전체 루프가 상수로 대체됩니다 (루프 수가 2000000001 인 경우에도 마찬가지 임). 내 의심 : 조기 최적화 (예 : 조기 최적화 (예 : 4 개의 배수, 언 롤링에 적합)) "이 루프와 거래 어쨌든 무엇입니까?"더 철저)
하겐 폰 Eitzen

답변:


88

TL; DR

기본적으로 현재 스냅 샷 GCC 7은 일관되지 않은 방식으로 작동하지만 이전 버전에는 PARAM_MAX_COMPLETELY_PEEL_TIMES 16 이 있습니다. 명령 줄에서 재정의 할 수 있습니다.

제한의 근거가 너무 공격적인 루프 언 롤링을 방지하는 것입니다, 그건 할 수있다 양날의 검 .

GCC 버전 <= 6.3.0

GCC에 대한 관련 최적화 옵션은 -fpeel-loops입니다 (플래그와 함께 간접적으로 활성화 됨 -Ofast).

프로파일 피드백 또는 정적 분석 에서 롤링하지 않는 충분한 정보가있는 루프 루프 . 또한 완전한 루프 필링 (예 : 일정한 반복 횟수로 루프를 완전히 제거)을 켭니다. .

-O3및 / 또는로 활성화됩니다 -fprofile-use.

자세한 내용은 추가하여 얻을 수 있습니다 -fdump-tree-cunroll .

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

메시지는 /gcc/tree-ssa-loop-ivcanon.c 같습니다.

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

그 후 try_peel_loop 함수는를 반환합니다 false.

더 자세한 출력은 -fdump-tree-cunroll-details .

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

max-completely-peeled-insns=nmax-completely-peel-times=n매개 변수를 사용하여 한계를 조정할 수 있습니다 .

max-completely-peeled-insns

완전히 벗겨진 루프의 최대 인스 턴 수입니다.

max-completely-peel-times

완전한 박리에 적합한 루프의 최대 반복 횟수입니다.

insns에 대한 자세한 내용은 GCC 내부 매뉴얼을 .

예를 들어 다음 옵션으로 컴파일하는 경우 :

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

그런 다음 코드는 다음과 같이 바뀝니다.

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

그 소리

Clang이 실제로 수행하는 작업과 한계를 조정하는 방법을 모르겠지만 관찰 한 바와 같이 unroll pragma로 루프를 표시하여 최종 값을 평가하도록 강제 할 수 있으며 완전히 제거합니다.

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

결과 :

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

이 멋진 답변에 감사드립니다. 다른 사람들이 지적했듯이 gcc는 정확한 한계 크기에 민감한 것으로 보입니다. 예를 들어 912 godbolt.org/g/EQJHvT 의 루프를 제거하지 못합니다 . 이 경우 fdump-tree-cunroll-details는 무엇을 말합니까?
eleanora 2019

실제로 200도이 문제가 있습니다. 이것은 godbolt가 제공하는 gcc 7의 스냅 샷입니다. godbolt.org/g/Vg3SVs 이것은 clang에는 전혀 적용되지 않습니다.
eleanora 2012

13
필링의 메커니즘을 설명하지만 960의 관련성이 무엇인지, 또는 전혀 한계가없는 이유는 설명하지 않습니다.
MM

1
@MM : GCC 6.3.0과 최신 스냅 호스트의 필링 동작은 완전히 다릅니다. 전자의 경우, 나는 하드 코딩 된 한계가 값 16으로 PARAM_MAX_COMPLETELY_PEEL_TIMES정의 된 param 에 의해 시행된다고 강력하게 의심합니다./gcc/params.def:321
Grzegorz Szpetkowski

14
GCC가 이러한 방식으로 의도적으로 제한하는 이유 를 언급 할 수 있습니다 . 특히 루프를 너무 적극적으로 풀면 바이너리가 커지고 L1 캐시에 적합하지 않습니다. 캐시 미스는 잠재적 분기 예측 (일반 루프의 경우)을 가정 할 때 몇 가지 조건부 점프를 저장하는 데 비해 상당히 비쌉니다 .
케빈

19

Sulthan의 의견을 읽은 후 다음과 같이 추측합니다.

  1. 루프 카운터가 일정하고 너무 높지 않은 경우 컴파일러에서 루프를 완전히 언롤합니다.

  2. 언 롤링되면 컴파일러는 합 연산이 하나로 그룹화 될 수 있음을 알게됩니다.

어떤 이유로 루프가 풀리지 않으면 (여기서 :로 너무 많은 명령문을 생성 함 1000) 조작을 그룹화 할 수 없습니다.

컴파일러 는 1000 개의 문을 언롤하는 것이 단일 추가에 해당한다는 것을 알 있지만 위에서 설명한 1 단계와 2 단계는 두 가지 별도의 최적화이므로 작업을 그룹화 할 수 있는지 알지 못하면 언 롤링의 "위험"을 취할 수 없습니다 (예 : 함수 호출은 그룹화 할 수 없습니다).

참고 : 이것은 모퉁이의 경우입니다. 누가 같은 것을 다시 추가하기 위해 루프를 사용합니까? 이 경우, 가능한 언 롤링 / 최적화 컴파일러에 의존하지 마십시오. 하나의 명령으로 올바른 작동을 직접 작성하십시오.


1
그런 not too high부분에 집중할 수 있습니까? 나는 왜 위험이 존재하지 100않는가? 나는 위의 내 의견에서 무언가를 추측했다. 그 이유가 될 수 있습니까?
user2736738

컴파일러가 트리거 할 수있는 부동 소수점 부정확성을 인식하지 못한다고 생각합니다. 나는 단지 명령 크기 제한이라고 생각합니다. 당신이 max-unrolled-insns함께max-unrolled-times
장 - 프랑수아 파브르

아아, 좀 더 분명한 추론을하는 것은 내 생각이나 추측이었다.
user2736738

5
사용자가 변경 흥미롭게 경우 floatint인해 유도 변수의 최적화에 관계없이, 반복 횟수의 루프 강도 줄이기 위해 GCC 컴파일러 수있다 ( -fivopts). 그러나 그것들은 작동하지 않는 것 같습니다 float.
Tavian Barnes 2012

1
@CortAmmon 맞습니다. GCC가 MPFR을 사용하여 매우 큰 수를 정확하게 계산하여 오류와 정밀도 손실이 누적되는 동등한 부동 소수점 연산과는 다른 결과를 얻는다는 사실에 놀랐고 화가 났던 사람들을 읽었습니다. 많은 사람들이 부동 소수점을 잘못된 방식으로 계산한다는 것을 보여줍니다.
Zan Lynx

12

아주 좋은 질문입니다!

코드를 단순화 할 때 컴파일러가 인라인하려고하는 반복 또는 작업 수에 한계가있는 것 같습니다. Grzegorz Szpetkowski가 문서화 한 것처럼 pragma 또는 명령 행 옵션으로 이러한 한계를 조정할 수있는 컴파일러 특정 방법이 있습니다.

또한 함께 재생할 수 있습니다 Godbolt의 컴파일러 탐색기 : 다른 컴파일러 옵션은 생성 된 코드에 미치는 영향을 비교 gcc 6.2하고 icc 17있는 반면, 960 인라인 여전히 코드를 clang 3.9(기본 Godbolt 구성, 실제로 73 인라인 중지로)하지 않습니다.


사용중인 gcc 및 clang 버전을 명확하게하기 위해 질문을 편집했습니다. godbolt.org/g/FfwWjL을 참조하십시오 . 예를 들어 -Ofast를 사용하고 있습니다.
eleanora
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.