sourcecode

버블 정렬이 GCC의 -O2보다 -O3의 -O3의 -O2일 때

copyscript 2022. 7. 27. 23:52
반응형

버블 정렬이 GCC의 -O2보다 -O3의 -O3의 -O2일 때

C에서 버블 정렬을 구현하고 성능을 테스트하고 있을 때 이 버블 정렬이-O3깃발은 깃발이 전혀 없는 것보다 더 느리게 작동하게 만들었다! ★★★★★★★★★★★★★★★★★.-O2기대했던 것보다 훨씬 더 빨리 작동하게 만든 것 같아요.

최적화 없음:

time ./sort 30000

./sort 30000  1.82s user 0.00s system 99% cpu 1.816 total

-O2:

time ./sort 30000

./sort 30000  1.00s user 0.00s system 99% cpu 1.005 total

-O3:

time ./sort 30000

./sort 30000  2.01s user 0.00s system 99% cpu 2.007 total

코드:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>

int n;

void bubblesort(int *buf)
{
    bool changed = true;
    for (int i = n; changed == true; i--) { /* will always move at least one element to its rightful place at the end, so can shorten the search by 1 each iteration */
        changed = false;

        for (int x = 0; x < i-1; x++) {
            if (buf[x] > buf[x+1]) {
                /* swap */
                int tmp = buf[x+1];
                buf[x+1] = buf[x];
                buf[x] = tmp;

                changed = true;
            }
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <arraysize>\n", argv[0]);
        return EXIT_FAILURE;
    }

    n = atoi(argv[1]);
    if (n < 1) {
        fprintf(stderr, "Invalid array size.\n");
        return EXIT_FAILURE;
    }

    int *buf = malloc(sizeof(int) * n);

    /* init buffer with random values */
    srand(time(NULL));
    for (int i = 0; i < n; i++)
        buf[i] = rand() % n + 1;

    bubblesort(buf);

    return EXIT_SUCCESS;
}

에 대해 -O2(godbolt.org에서):

bubblesort:
        mov     r9d, DWORD PTR n[rip]
        xor     edx, edx
        xor     r10d, r10d
.L2:
        lea     r8d, [r9-1]
        cmp     r8d, edx
        jle     .L13
.L5:
        movsx   rax, edx
        lea     rax, [rdi+rax*4]
.L4:
        mov     esi, DWORD PTR [rax]
        mov     ecx, DWORD PTR [rax+4]
        add     edx, 1
        cmp     esi, ecx
        jle     .L2
        mov     DWORD PTR [rax+4], esi
        mov     r10d, 1
        add     rax, 4
        mov     DWORD PTR [rax-4], ecx
        cmp     r8d, edx
        jg      .L4
        mov     r9d, r8d
        xor     edx, edx
        xor     r10d, r10d
        lea     r8d, [r9-1]
        cmp     r8d, edx
        jg      .L5
.L13:
        test    r10b, r10b
        jne     .L14
.L1:
        ret
.L14:
        lea     eax, [r9-2]
        cmp     r9d, 2
        jle     .L1
        mov     r9d, r8d
        xor     edx, edx
        mov     r8d, eax
        xor     r10d, r10d
        jmp     .L5

-O3:

bubblesort:
        mov     r9d, DWORD PTR n[rip]
        xor     edx, edx
        xor     r10d, r10d
.L2:
        lea     r8d, [r9-1]
        cmp     r8d, edx
        jle     .L13
.L5:
        movsx   rax, edx
        lea     rcx, [rdi+rax*4]
.L4:
        movq    xmm0, QWORD PTR [rcx]
        add     edx, 1
        pshufd  xmm2, xmm0, 0xe5
        movd    esi, xmm0
        movd    eax, xmm2
        pshufd  xmm1, xmm0, 225
        cmp     esi, eax
        jle     .L2
        movq    QWORD PTR [rcx], xmm1
        mov     r10d, 1
        add     rcx, 4
        cmp     r8d, edx
        jg      .L4
        mov     r9d, r8d
        xor     edx, edx
        xor     r10d, r10d
        lea     r8d, [r9-1]
        cmp     r8d, edx
        jg      .L5
.L13:
        test    r10b, r10b
        jne     .L14
.L1:
        ret
.L14:
        lea     eax, [r9-2]
        cmp     r9d, 2
        jle     .L1
        mov     r9d, r8d
        xor     edx, edx
        mov     r8d, eax
        xor     r10d, r10d
        jmp     .L5

나에게 중요한 유일한 차이점은 SIMD를 사용하려는 명백한 시도인 것 같습니다. 이것은 큰 개선이 될 것 같지만, 나는 또한 그것들로 대체 무엇을 시도하는지 알 수 없습니다.pshufd... 이건 요? 추가 명령이 명령캐시를 일 수도 .SIMD? 아니면 명령 캐시를 조금씩 제거하는 것 뿐인가요?

타이밍은 AMD Ryzen 5 3600으로 설정되었습니다.

GCC의 스토어 포워딩 스톨에 대한 순진함이 이곳의 자동 벡터화 전략에 타격을 주고 있는 것 같다.하드웨어 퍼포먼스 카운터를 사용한 인텔의 실제 벤치마크에 대해서는, 「Store Forwarding by hardware performance counters를 참조해 주세요.또, x86에서의 스토어 투 로드 전송에 실패했을 경우의 코스트는 얼마입니까?또한 Agner Fog의 x86 최적화 가이드.

)gcc -O3를 유효하게 하다-ftree-vectorize 에 되지 않은 몇 옵션-O2 예 , ) 。if- - '브런치리스'cmov는 GCC가 예상하지 못한 데이터 패턴에 피해를 있는다른 방법입니다.이에 비해 Clang은 자동벡터라이제이션이 가능합니다.-O2 그 「」로 하게 되어 .-O3

64비트 로드(및 저장 여부에 관계없이 분기)를 int 쌍으로 수행합니다.즉, 마지막 반복을 스왑하면 이 로드의 절반은 해당 저장소에서, 절반은 신규 메모리에서 발생하므로 스왑할 때마다 스토어 포워딩 스톨이 발생합니다.그러나 버블 정렬은 종종 요소 버블처럼 모든 반복을 교환하는 긴 체인을 가지고 있기 때문에 이는 매우 좋지 않습니다.

(버블 정렬은 일반적으로 좋지 않습니다.특히 이전 반복의 두 번째 요소를 레지스터에 보관하지 않고 순진하게 구현된 경우에는 더욱 그렇습니다.asm의 상세 내용을 분석하면 재미있어지기 때문에 시도해 봐도 무방합니다.)

어쨌든 이것은 GCC Bugzilla에서 "missed-optimization" 키워드를 사용하여 보고해야 하는 안티 최적화입니다.스칼라 로드는 저렴하고 스토어 포워딩은 비용이 많이 듭니다.(현대판 x86 구현은 여러 이전 스토어에서 스토어 포워딩이 가능합니까?또한 Atom 이외마이크로아키텍처는 이전 스토어 중 하나와 부분적으로 중복되거나 L1d 캐시에서 전송되어야 하는 데이터에서 효율적으로 로딩할 수 없습니다.)

좋은 은 계속 가지고 있는 이다.buf[x+1]buf[x]저장 및 로드를 피할 수 있습니다.(수기 ASM 버블 정렬의 좋은 예와 같이, 그 중 일부는 스택 오버플로에 존재합니다).

(AFIK GCC가 코스트 모델에서는 알 수 없는) 스토어 포워딩 스토어가 아니었다면, 이 전략은 손익분기점에 관한 것이었을지도 모른다.SSE 4.1(브런치리스용)pmindpmaxdComparator는 흥미롭지만 항상 저장해야 하며 C 소스에서는 그렇지 않습니다.


이 배폭 부하 전략이 장점이 있다면 x86-64와 같은 64비트 머신에 순수 정수를 사용하여 구현하면 좋을 것입니다.x86-64에서는 상위 절반의 가비지(또는 귀중한 데이터)가 있는 하위 32비트만 조작할 수 있습니다.예.,

## What GCC should have done,
## if it was going to use this 64-bit load strategy at all

        movsx   rax, edx           # apparently it wasn't able to optimize away your half-width signed loop counter into pointer math
        lea     rcx, [rdi+rax*4]   # Usually not worth an extra instruction just to avoid an indexed load and indexed store, but let's keep it for easy comparison.
.L4:
        mov     rax, [rcx]       # into RAX instead of XMM0
        add     edx, 1
            #  pshufd  xmm2, xmm0, 0xe5
            #  movd    esi, xmm0
            #  movd    eax, xmm2
            #  pshufd  xmm1, xmm0, 225
        mov     rsi, rax
        rol     rax, 32   # swap halves, just like the pshufd
        cmp     esi, eax  # or eax, esi?  I didn't check which is which
        jle     .L2
        movq    QWORD PTR [rcx], rax   # conditionally store the swapped qword

BMI2를 ('BMI2'에서 )-march=native,rorx rsi, rax, 32이데올로기 때문에사용하지 않을 BMI2는mov복사 대신 원본 파일을 스왑하면 Ice Lake와 같은 이동 제거 기능이 없는 CPU에서 실행되는 경우 지연 시간이 줄어듭니다.)

따라서 비교할 부하의 총 지연 시간은 정수 부하 + 1개의 ALU 연산(회전)입니다.로드 비교 -> XMM 부부 ->movd그리고 ALUUUP도 적습니다.그러나 이것은 스토어 포워딩 스톨 문제에 전혀 도움이 되지 않습니다.이것은 여전히 쇼스토퍼입니다.이는 pshufd x 2와 pshufd x 2를 대체하는 동일한 전략의 정수 SWAR 구현에 불과합니다.movd r32, xmmmov+rol.

은 쓸 pshufd레지스터를 와 XMM의 양쪽 할 수 .movdXMM regs는 XMM regs입니다. 의 두 부분이 두 의 GCC를 했습니다.pshufd지시사항; 하나는 셔플 상수를 16진수로 인쇄하고 다른 하나는 10진수로 인쇄합니다!하려고 것 .vec[1]qword의 입니다.


전혀 깃발이 없는 것보다 느리다

은 "Default"-O0C 스테이트먼트마다 모든 변수를 메모리에 흘려보내는 일관된 디버깅모드입니다.따라서 상당히 끔찍하고 큰 스토어 포워딩 레이텐시의 보틀 넥이 발생합니다.(모든 변수가 다음과 같은 경우)volatile그러나 스토어 포워딩은 성공적이어서 정지하지 않고 최대 5사이클에 불과하지만 레지스터의 경우 0보다 훨씬 심각합니다(Zen 2를 비롯한 일부 최신 마이크로 아키텍처는 지연 시간이 짧은 특수한 경우가 있습니다).파이프라인을 통과해야 하는 추가 저장 및 적재 지침은 도움이 되지 않습니다.

으로 벤치마킹하는 -O0-O1 ★★★★★★★★★★★★★★★★★」-Og컴파일러가 일반인이 기대하는 최적화의 기본적인 양을 실행하기 위한 기준선이 되어야 합니다.단, 레지스터 할당을 건너뛰어 asm을 의도적으로 강요해서는 안 됩니다.


반관련성: 속도가 아닌 크기에 맞게 버블 정렬을 최적화하려면 메모리 대상 회전(백투백 스왑을 위한 스토어 포워딩 스톨 생성) 또는 메모리 대상이 필요할 수 있습니다.xchg (비밀(이행)lockprefix -> 매우 느립니다).이 Code Golf 답변을 참조하십시오.

언급URL : https://stackoverflow.com/questions/69503317/bubble-sort-slower-with-o3-than-o2-with-gcc

반응형