인라인 어셈블리 주석을 추가하면 GCC에서 생성 된 코드가 크게 변경되는 이유는 무엇입니까?


82

그래서 다음 코드가 있습니다.

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        *sum++ = *a++ + *b++;
    }
}

GCC 4.7.2가 생성하는 코드를보고 싶었습니다. 그래서 나는 g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11다음과 같은 출력을 얻었습니다.

        .file   "a.c++"
        .intel_syntax noprefix
        .text
        .p2align 4,,15
        .globl  _Z2f1PcS_S_
        .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L5
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L5
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L3:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L3
        mov     ax, 8
        mov     r9d, 992
.L2:
        sub     eax, 1
        lea     rcx, [rdx+r9]
        add     rdi, r9
        lea     r8, [rax+1]
        add     rsi, r9
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L4:
        movzx   edx, BYTE PTR [rcx+rax]
        add     dl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], dl
        add     rax, 1
        cmp     rax, r8
        jne     .L4
        rep
        ret
.L5:
        mov     eax, 1000
        xor     r9d, r9d
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
        .p2align 4,,15
        .globl  _Z2f2PcS_S_
        .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L19
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L19
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L17:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L17
        add     rdi, 992
        add     rsi, 992
        add     rdx, 992
        mov     r8d, 8
.L16:
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L18:
        movzx   ecx, BYTE PTR [rdx+rax]
        add     cl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, r8
        jne     .L18
        rep
        ret
.L19:
        mov     r8d, 1000
        jmp     .L16
        .cfi_endproc
.LFE1:
        .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
        .ident  "GCC: (GNU) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

어셈블리를 읽는 것이 싫기 때문에 루프의 몸체가 어디로 갔는지 알기 위해 마커를 추가하기로 결정했습니다.

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        asm("# im in ur loop");
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        asm("# im in ur loop");
        *sum++ = *a++ + *b++;
    }
}

그리고 GCC는 다음과 같이 말했습니다.

    .file   "a.c++"
    .intel_syntax noprefix
    .text
    .p2align 4,,15
    .globl  _Z2f1PcS_S_
    .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L2:
#APP
# 4 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L2
    rep
    ret
    .cfi_endproc
.LFE0:
    .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
    .p2align 4,,15
    .globl  _Z2f2PcS_S_
    .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L6:
#APP
# 12 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L6
    rep
    ret
    .cfi_endproc
.LFE1:
    .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

이것은 상당히 짧으며 SIMD 명령이없는 것과 같은 몇 가지 중요한 차이점이 있습니다. 나는 중간 어딘가에 몇 가지 주석과 함께 동일한 결과를 기대하고 있었다. 내가 여기서 잘못된 가정을하고 있습니까? GCC의 옵티마이 저가 asm 주석에 의해 방해를 받습니까?


28
GCC (및 대부분의 컴파일러)가 ASM 구조를 블록 상자처럼 취급 할 것으로 기대합니다. 그래서 그들은 그러한 상자를 통해 일어나는 일에 대해 추론 할 수 없습니다. 그리고 이는 많은 최적화를 방해합니다. 특히 루프 경계를 넘어서 수행됩니다.
Ira Baxter

10
asm빈 출력과 클로버 목록이있는 확장 양식을 사용해보십시오 .
Kerrek SB

4
@ R.MartinhoFernandes : asm("# im in ur loop" : : );(참조 설명서 )
마이크 시모어

16
-fverbose-asm플래그 를 추가하여 생성 된 어셈블리를 볼 때 약간의 도움을받을 수 있습니다.이 플래그는 레지스터간에 사물이 이동하는 방식을 식별하는 데 도움이되는 주석을 추가합니다.
Matthew Slattery

1
매우 흥미로운. 루프에서 최적화를 선택적으로 피하는 데 사용할 수 있습니까?
SChepurin

답변:


62

최적화와의 상호 작용은 설명서"C 표현식 피연산자를 사용한 어셈블러 지침" 페이지 중간에 설명 되어 있습니다.

GCC는 내부의 실제 어셈블리를 이해하려고하지 않습니다 asm. 콘텐츠에 대해 아는 유일한 것은 출력 및 입력 피연산자 사양과 레지스터 클로버 목록에서 (선택적으로) 알려주는 것입니다.

특히 다음을 참고하십시오.

asm모든 출력없이 피연산자 명령은 휘발성 동일하게 처리 될 asm명령.

volatile키워드 명령 중요한 부작용을 갖는 것을 나타낸다 [...]

따라서 asm루프 내부 의 존재는 GCC가 부작용이 있다고 가정하기 때문에 벡터화 최적화를 방해했습니다.


1
Basic Asm 문의 부작용에는 레지스터 수정이나 C ++ 코드가 읽고 쓰는 메모리가 포함되어서는 안됩니다. 그러나 예, asm명령문은 C ++ 추상 머신에서 매번 실행되어야하며 GCC는 벡터화하지 않고 asm을 paddb. char 액세스가 아니기 때문에 합법적이라고 생각합니다 volatile. (클로버가있는 확장 된 asm 문과 달리 "memory")
Peter Cordes

1
일반적으로 GNU C Basic Asm 문을 사용하지 않는 이유는 gcc.gnu.org/wiki/ConvertBasicAsmToExtended 를 참조하십시오 . 이 사용 사례 (단지 주석 표시 자)는 시도하는 것이 합리적이지 않은 몇 안되는 사례 중 하나입니다.
Peter Cordes

23

gcc는 코드를 벡터화하여 루프 본문을 두 부분으로 분할했습니다. 첫 번째는 한 번에 16 개의 항목을 처리하고 두 번째는 나중에 나머지를 처리합니다.

Ira가 언급했듯이 컴파일러는 asm 블록을 구문 분석하지 않으므로 주석 일 뿐이라는 것을 알지 못합니다. 그래도 의도 한 바를 알 수있는 방법이 없습니다. 최적화 된 루프는 몸체를 두 배로 늘 렸습니다. 각각에 asm을 넣어야합니까? 1000 번 실행하지 않기를 원하십니까? 알지 못하므로 안전한 경로로 이동하고 간단한 단일 루프로 돌아갑니다.


3

나는 "gcc가 asm()블록 에있는 것을 이해하지 못한다"에 동의하지 않습니다 . 예를 들어, gcc는 매개 변수를 최적화 asm()하고 생성 된 C 코드와 섞이도 록 블록을 다시 배열하는 작업을 아주 잘 처리 할 수 ​​있습니다 . 예를 들어 Linux 커널에서 인라인 어셈블러를 보면 __volatile__컴파일러가 "코드를 이동하지 않음"을 확인하기 위해 거의 항상 접두사가 붙습니다 . 나는 gcc가 내 "rdtsc"를 움직여서 특정 작업을 수행하는 데 걸리는 시간을 측정했습니다.

문서화 된 것처럼 gcc는 특정 유형의 asm()블록을 "특별한"것으로 취급 하므로 블록의 양쪽에서 코드를 최적화하지 않습니다.

그것은 gcc가 때때로 인라인 어셈블러 블록에 의해 혼동되지 않거나 어셈블러 코드 등의 결과를 따를 수 없기 때문에 특정 최적화를 포기하기로 결정하지 않는다는 의미가 아닙니다. 더 중요한 것은 종종 clobber 태그가 누락되어 혼동 될 수 있습니다. 따라서 다음과 같은 지침이있는 경우cpuidEAX-EDX의 값을 변경하지만 EAX 만 사용하도록 코드를 작성하면 컴파일러가 EBX, ECX 및 EDX에 항목을 저장할 수 있으며 이러한 레지스터를 덮어 쓰면 코드가 매우 이상하게 작동합니다. 운이 좋으면 즉시 충돌이 발생합니다. 그러면 무슨 일이 일어나는지 쉽게 파악할 수 있습니다. 그러나 만약 당신이 운이 좋지 않다면, 그것은 라인 아래로 충돌합니다. 또 다른 까다로운 것은 edx에서 두 번째 결과를 제공하는 나누기 명령입니다. 모듈로에 관심이 없다면 EDX가 변경되었다는 사실을 잊기 쉽습니다.


1
gcc는 실제로 asm 블록에있는 내용을 이해하지 못합니다. 확장 된 asm 문을 통해 알려야합니다. 이 추가 정보가 없으면 gcc는 이러한 블록을 이동하지 않습니다. gcc는 또한 당신이 언급 한 경우에 혼란스럽지 않습니다. 당신은 gcc가 실제로 그 레지스터를 사용할 수 있다고 말함으로써 프로그래밍 오류를 만들었습니다.
모니카 기억

답변이 늦었지만 말할 가치가 있다고 생각합니다. volatile asmGCC에 코드에 '중요한 부작용'이있을 수 있으며 더 특별한주의를 기울여 처리 할 것입니다. 데드 코드 최적화의 일부로 삭제되거나 제거 될 수 있습니다 . C 코드와의 상호 작용은 그러한 (드문) 경우를 가정하고 엄격한 순차적 평가를 부과해야합니다 (예 : asm 내에서 종속성 생성).
edmz

GNU C Basic asm (OP와 같은 피연산자 제약 없음 asm(""))은 출력 피연산자가없는 Extended asm과 마찬가지로 암시 적으로 휘발성입니다. GCC는 asm 템플릿 문자열을 이해하지 못하고 제약 조건 만 이해합니다. 그렇기 때문에 제약 조건을 사용하여 컴파일러에 asm을 정확하고 완전하게 설명하는 것이 중요 합니다. 피연산자를 템플릿 문자열로 대체 printf하는 것은 형식 문자열을 사용하는 것보다 더 이해가 필요하지 않습니다 . TL : DR : 순수한 주석으로 이와 같은 유스 케이스를 제외하고는 GNU C Basic asm을 아무것도 사용하지 마십시오.
Peter Cordes

-2

이 답변은 이제 수정되었습니다. 원래는 인라인 Basic Asm을 매우 강력하게 지정된 도구로 고려하는 사고 방식으로 작성되었지만 GCC에서는 그렇지 않습니다. Basic Asm은 약해서 답변이 수정되었습니다.

각 어셈블리 주석은 중단 점 역할을합니다.

편집 : 그러나 Basic Asm을 사용함에 따라 깨진 것입니다. 명시적인 clobber 목록이없는 인라인 asm( asm함수 본문 내부 의 문)은 GCC에서 약하게 지정된 기능이며 그 동작을 정의하기 어렵습니다. 특히 어떤 것에 첨부 된 것 같지 않습니다 (그 보증을 완전히 이해하지 못합니다). 따라서 함수가 실행되면 어셈블리 코드는 어느 시점에서 실행되어야하지만, 어떤 비 사소한 최적화 수준 . 인접 명령어로 재정렬 할 수있는 중단 점은 매우 유용한 "중단 점"이 아닙니다. 편집 종료

각 주석에서 중단되고 모든 변수의 상태를 출력하는 인터프리터에서 프로그램을 실행할 수 있습니다 (디버그 정보 사용). 환경 (레지스터 및 메모리 상태)을 관찰하려면 이러한 지점이 존재해야합니다.

주석이 없으면 관찰 지점이 존재하지 않으며 루프는 환경을 가져와 수정 된 환경을 생성하는 단일 수학 함수로 컴파일됩니다.

무의미한 질문에 대한 답을 알고 싶습니다. 각 명령어 (또는 블록 또는 명령어 범위)가 어떻게 컴파일되는지 알고 싶지만 단일 절연 명령어 (또는 블록)는 컴파일되지 않습니다. 모든 것이 전체적으로 컴파일됩니다.

더 나은 질문은 다음과 같습니다.

안녕하세요 GCC. 이 asm 출력이 소스 코드를 구현한다고 생각하는 이유는 무엇입니까? 모든 가정과 함께 단계별로 설명하십시오.

그러나 GCC 내부 표현이라는 용어로 작성된 asm 출력보다 긴 증명을 읽고 싶지 않을 것입니다.


1
환경 (레지스터 및 메모리 상태)을 관찰하려면 이러한 지점이 존재해야합니다. -이것은 최적화되지 않은 코드에 해당 될 수 있습니다. 최적화가 활성화되면 전체 기능이 바이너리에서 사라질 수 있습니다. 여기서는 최적화 된 코드에 대해 이야기하고 있습니다.
Bartek Banachewicz 2015 년

1
최적화가 활성화 된 상태에서 컴파일 한 결과 생성 된 어셈블리에 대해 이야기하고 있습니다. 그러므로 당신은 무엇이 든지 존재해야한다고 말하는 것이 잘못 되었습니다.
Bartek Banachewicz 2015 년

1
그래, IDK는 왜 누구라도 그렇게했고 아무도 안된다는 데 동의합니다. 내 마지막 댓글의 링크에서 설명했듯이, 누구도 그렇게해서는 안되며, "memory"확실히 존재하는 기존 버그 코드에 대한 반창고로 강화 (예 : 묵시적 clobber 사용) 에 대한 논쟁 이있었습니다. 이와 같은 명령 asm("cli")이 컴파일러 생성 코드가 건드리지 않는 아키텍처 상태의 일부에만 영향을 미치더라도 여전히 주문 된 wrt가 필요합니다. 컴파일러가 생성 한로드 / 저장 (예 : 중요 섹션 주변의 인터럽트를 비활성화하는 경우).
Peter Cordes

1
레드 존을 무너 뜨리는 것이 안전하지 않기 때문에 asm 문 내부의 비효율적 인 수동 저장 / 레지스터 복원 (푸시 / 팝 사용)도 add rsp, -128처음 이 아니면 안전하지 않습니다 . 하지만 그렇게하는 것은 분명히 뇌사입니다.
Peter Cordes

1
현재 GCC는 Basic Asm을 정확히 동일하게 처리합니다 asm("" :::)(출력이 없기 때문에 암시 적으로 휘발성이지만 입력 또는 출력 종속성에 의해 나머지 코드에 연결되지 않습니다 "memory". 물론 %operand템플릿 문자열을 대체 %하지 않으므로 리터럴 을 %%. 예, 동의했습니다. __attribute__((naked))함수와 전역 범위 밖에서 Basic Asm을 사용하지 않는 것이 좋습니다.
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.