컴파일러가 예측 가능한 추가 루프를 곱셈으로 최적화 할 수없는 이유는 무엇입니까?


133

이것은 질문에 대한 Mysticial 의 훌륭한 답변을 읽는 동안 염두에 두었던 질문입니다. 정렬되지 않은 배열보다 정렬 된 배열을 처리하는 것이 왜 더 빠릅 니까?

관련된 유형에 대한 컨텍스트 :

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

그의 대답에서 그는 인텔 컴파일러 (ICC)가 이것을 최적화한다고 설명합니다.

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

... 이와 동등한 것으로 :

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

옵티마이 저는 이들이 동등 함을 인식 하고 루프를 교환 하여 분기를 내부 루프 밖으로 이동시킵니다. 매우 영리한!

그러나 왜 그렇지 않습니까?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

바라건대 Mysticial (또는 다른 사람)도 똑같이 훌륭한 답변을 줄 수 있습니다. 나는 이전에 다른 질문에서 논의 된 최적화에 대해 배운 적이 없으므로 정말 감사합니다.


14
아마 인텔 만이 아는 것입니다. 최적화 패스를 실행하는 순서를 모르겠습니다. 그리고 분명히 루프 교환 후 루프 축소 패스를 실행하지 않습니다.
Mysticial

7
이 최적화는 데이터 배열에 포함 된 값을 변경할 수없는 경우에만 유효합니다. (가) 인 경우 예를 들어, 메모리 매핑 입력 / 출력 장치는 데이터 [0] ... 다른 값을 생성 할 때마다 판독
토마스 CG 드 헤나

2
정수 또는 부동 소수점 데이터 유형은 무엇입니까? 부동 소수점을 반복해서 더하면 곱셈과 매우 다른 결과가 나타납니다.
Ben Voigt 2016 년

6
@Thomas : 데이터가 volatile인 경우 루프 교환도 잘못된 최적화입니다.
Ben Voigt 2016 년

3
GNAT (GCC 4.6의 Ada 컴파일러)는 O3에서 루프를 전환하지 않지만 루프가 전환되면 루프를 곱셈으로 변환합니다.
prosfilaes

답변:


105

컴파일러는 일반적으로 변환 할 수 없습니다

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

으로

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

후자는 전자가하지 않은 부호있는 정수의 오버플로를 일으킬 수 있기 때문입니다. 부호있는 2의 보수 정수의 오버플로에 대해 보장 된 랩 어라운드 동작으로도 결과가 변경됩니다 data[c](30000 인 경우 제품은 랩핑이 -1294967296있는 전형적인 32 비트 ints가되고 100000 번 추가하면 30000이 추가 sum됩니다) 오버플로하지 않고 sum3000000000 증가 ). 숫자가 다른 부호없는 수량에 대해서도 동일하게 유지되므로 오버플로 100000 * data[c]는 일반적으로 2^32최종 결과에 나타나지 않아야 하는 감소 모듈로 를 발생시킵니다.

그것은 그것을

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

그러나 평소와 같이 long long충분히 큽니다 int.

그렇게하지 않는 이유는 알 수 없다. 나는 Mysticial이 "루프 교환 후 루프 붕괴 패스를 실행하지 않는다"고 말했다.

루프 교환 자체는 일반적으로 유효하지 않습니다 (서명 된 정수의 경우).

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

오버플로로 이어질 수 있습니다

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

그렇지 않습니다. 조건 data[c]이 추가 되면 모든 항목 이 동일한 부호를 갖도록 보장 되므로 하나가 넘치면 둘 다 수행됩니다.

컴파일러가 그것을 고려했다고 확신하지는 못합니다 (@Mysticial, data[c] & 0x80양수 및 음수 값에 해당 하는 조건으로 시도해 볼 수 있습니까?). 나는 컴파일러 (예를 들어, 몇 년 전에, 나는 IIRC, ICC는 (11.0했다) 무효 최적화 할 로그인 32 비트-INT-에 두 번에 변환 사용했다 1.0/n어디 n을 하였다 unsigned int. GCC 년대로 두 배 정도 빨랐다 그러나 잘못된 결과는 많은 것보다 컸습니다 2^31.


4
32K보다 큰 스택 프레임을 허용하는 옵션을 추가 한 MPW 컴파일러 버전을 기억합니다 (이전 버전은 로컬 변수에 @ A7 + int16 주소 지정을 사용하여 제한됨). 32K 이하 또는 64K 이상의 스택 프레임에 대해서는 모든 것이 가능하지만 40K 스택 프레임의 경우 ADD.W A6,$A000주소 레지스터를 사용한 워드 연산이 워드를 추가하기 전에 32 비트로 부호 확장한다는 것을 잊어 버립니다. 코드가 ADD그 다음에 스택에서 A6을 튀어
나왔을 때 유일한

3
... 호출자가 처리 한 레지스터는 정적 배열의 [load-time constant] 주소뿐입니다. 컴파일러는 배열의 주소가 레지스터에 저장되어이를 기반으로 최적화 할 수 있다는 것을 알고 있었지만 디버거는 상수의 주소 만 알았습니다. 따라서 진술 전에 MyArray[0] = 4;나는의 주소를 검사 MyArray하고 진술이 실행되기 전후의 해당 위치를 볼 수 있었다 . 변경되지 않습니다. 코드는 move.B @A3,#4같았고 A3는 항상 MyArray명령이 실행 된 시간을 가리켜 야했지만 그렇지 않았습니다. 장난.
supercat

그렇다면 clang은 왜 이런 종류의 최적화를 수행합니까?
Jason S

컴파일러는 내부 중간 표현에서 정의되지 않은 동작을 가질 수 있기 때문에 내부 중간 표현에서 재 작성을 수행 할 수 있습니다.
253751

48

이 답변은 링크 된 특정 사례에는 적용되지 않지만 질문 제목에는 적용되며 향후 독자에게 흥미로울 수 있습니다.

유한 정밀도로 인해 반복 부동 소수점 추가는 곱셈과 동일하지 않습니다 . 치다:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

데모


10
이것은 질문에 대한 답이 아닙니다. 흥미로운 정보 (및 C / C ++ 프로그래머에게 알아야 할 사항)에도 불구하고이 포럼은 포럼이 아니며 여기에 속하지 않습니다.
orlp

30
@nightcracker : StackOverflow의 목표는 향후 사용자에게 유용한 검색 가능한 답변 라이브러리를 구축하는 것입니다. 그리고 이것은 묻는 질문에 대한 답변입니다 ...이 답변이 원래 포스터에 적용되지 않도록하는 언급되지 않은 정보가 있습니다. 같은 질문을 가진 다른 사람들에게도 여전히 적용될 수 있습니다.
Ben Voigt 2016 년

12
그것은의 질문에 대한 해답이 될 제목 아니, 아닌 질문입니다.
orlp

7
내가 말했듯이, 그것은 흥미로운 정보입니다. 그러나 여전히 질문 의 상위 답변 이 그 질문에 대한 얻지 못하는 것은 여전히 ​​잘못된 것 같습니다 . 이것이 바로 인텔 컴파일러가 최적화하지 않기로 결정한 이유는 아닙니다.
orlp

4
@ nightcracker : 이것이 최고의 대답이라는 것도 나에게 잘못된 것 같습니다. 누군가이 점수를 능가하는 정수 케이스에 대해 정말 좋은 답변을 게시하기를 바랍니다. 불행히도, 정수의 경우 "할 수 없음"에 대한 답이 없다고 생각합니다. 변환이 합법적 일 수 있기 때문에 "그렇지 않은 이유"가 남아 있습니다. 실제로 " 특정 컴파일러 버전에 고유하기 때문에 너무 현지화 된 "가까운 이유. 내가 대답 한 질문은 더 중요한 IMO입니다.
Ben Voigt 2016 년

6

컴파일러에는 최적화를 수행하는 다양한 패스가 포함되어 있습니다. 일반적으로 각 단계에서 명령문에 대한 최적화 또는 루프 최적화가 수행됩니다. 현재 루프 헤더를 기반으로 루프 바디를 최적화하는 모델은 없습니다. 이것은 감지하기 어렵고 덜 일반적입니다.

수행 된 최적화는 루프 불변 코드 모션이었습니다. 이것은 일련의 기술을 사용하여 수행 할 수 있습니다.


4

글쎄, 우리는 정수 산술에 대해 이야기하고 있다고 가정 할 때 일부 컴파일러가 이러한 종류의 최적화를 수행 할 수 있다고 생각합니다.

동시에 반복적으로 더하기를 곱셈으로 바꾸면 코드의 오버플로 동작이 변경 될 수 있으므로 일부 컴파일러는이를 거부 할 수 있습니다. 부호없는 정수 유형의 경우 오버플로 동작이 언어에 의해 완전히 지정되므로 차이가 없어야합니다. 그러나 서명 된 사람들에게는 아마도 2의 보완 플랫폼에는 없을 것입니다. 실제로 부호있는 오버플로가 C에서 정의되지 않은 동작을 초래한다는 것은 사실입니다. 즉, 오버플로 의미를 완전히 무시한다는 것은 완벽하게 괜찮지 만 모든 컴파일러가 그렇게 용감하지는 않습니다. 그것은 종종 "C는 단지 고수준의 어셈블리 언어"군중들로부터 많은 비판을받습니다. (GCC가 엄격한 앨리어싱 의미론을 기반으로 최적화를 도입했을 때 일어난 일을 기억하십니까?)

역사적으로 GCC는 그처럼 과감한 조치를 취하는 데 필요한 컴파일러로 나타 났지만 다른 컴파일러는 언어에 의해 정의되지 않은 경우에도 인식 된 "사용자 의도"동작을 고수하는 것을 선호 할 수 있습니다.


실수로 정의되지 않은 동작에 의존하고 있는지 알고 싶지만 오버플로가 런타임 문제 일 것이므로 컴파일러가 알 수있는 방법이 없다고 생각합니다 : /
jhabbott

2
@jhabbott : IFF에 오버 플로우가 발생하고 정의되지 않은 동작이있다. 동작이 정의되는지 여부는 런타임까지 알 수 없습니다 (런타임에 숫자가 입력되었다고 가정) : P.
orlp

3

이제는 최소한 clang은 다음과 같이합니다.

long long add_100k_signed(int *data, int arraySize)
{
    long long sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

-O1을 사용하여 컴파일

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        movsxd  rdx, dword ptr [rdi + 4*rsi]
        imul    rcx, rdx, 100000
        cmp     rdx, 127
        cmovle  rcx, r8
        add     rax, rcx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

정수 오버플로는 이와 관련이 없습니다. 정의되지 않은 동작을 일으키는 정수 오버플로가있는 경우 어느 경우 든 발생할 수 있습니다. 다음 대신 사용하는 동일한 종류의 함수 int가 있습니다long .

int add_100k_signed(int *data, int arraySize)
{
    int sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

-O1을 사용하여 컴파일

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        mov     edx, dword ptr [rdi + 4*rsi]
        imul    ecx, edx, 100000
        cmp     edx, 127
        cmovle  ecx, r8d
        add     eax, ecx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

2

이런 종류의 최적화에는 개념적 장벽이 있습니다. 컴파일러 작성자 는 곱셈을 더하기 및 바꾸기로 바꾸는 등 강도 감소 에 많은 노력을 기울입니다 . 그들은 곱셈이 나쁘다는 생각에 익숙해집니다. 따라서 다른 방법으로 가야하는 경우는 놀랍고 반 직관적입니다. 따라서 아무도 그것을 구현하려고 생각하지 않습니다.


3
루프를 닫힌 양식 계산으로 바꾸는 것도 강도 감소입니다.
Ben Voigt 2016 년

공식적으로는 그렇습니다. 그러나 저는 그런 식으로 말하는 사람은 없습니다. (그러나 나는 문헌에 약간의 구식이다.)
zwol

1

컴파일러를 개발하고 유지 관리하는 사람들은 작업에 소요되는 시간과 에너지가 제한되어 있으므로 일반적으로 사용자가 가장 관심을 갖는 것에 초점을 맞추고 싶습니다. 잘 작성된 코드를 빠른 코드로 전환하는 것입니다. 그들은 바보 같은 코드를 빠른 코드로 바꾸는 방법을 찾기 위해 시간을 허비하기를 원하지 않습니다. 이것이 바로 코드 검토입니다. 고급 언어에는 중요한 아이디어를 표현하는 "어리석은"코드가있을 수 있습니다. 예를 들어, 짧은 삼림 벌채 및 스트림 융합은 Haskell 프로그램이 특정 종류의 게으른 주위에 구조화 될 수있게 해줍니다. 메모리를 할당하지 않는 타이트한 루프로 컴파일 할 데이터 구조를 생성했습니다. 그러나 이러한 종류의 인센티브는 단순히 반복 덧셈을 곱셈으로 바꾸는 데 적용되지 않습니다. 빨리하고 싶다면

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