한계가 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=n및 max-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
사용자가 변경 흥미롭게 경우 float로 int인해 유도 변수의 최적화에 관계없이, 반복 횟수의 루프 강도 줄이기 위해 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.