컴파일러가 여기에 수신자 저장 레지스터 사용을 고집하는 이유는 무엇입니까?


10

이 C 코드를 고려하십시오.

void foo(void);

long bar(long x) {
    foo();
    return x;
}

GCC 9.3에서 -O3또는로 컴파일하면 다음과 -Os같이 나타납니다.

bar:
        push    r12
        mov     r12, rdi
        call    foo
        mov     rax, r12
        pop     r12
        ret

clang의 출력 은 수신자 저장 레지스터 rbx대신 선택을 제외하고 동일합니다 r12.

그러나 다음과 같은 어셈블리를보고 싶습니다.

bar:
        push    rdi
        call    foo
        pop     rax
        ret

영어로, 내가 겪고있는 것을 다음과 같습니다 :

  • 수신자 저장 레지스터의 이전 값을 스택으로 푸시
  • x해당 수신자 저장 레지스터로 이동
  • 요구 foo
  • 이동 x리턴 값 레지스터로 호출 수신자 저장 한 레지스터에서
  • 수신자 저장 레지스터의 이전 값을 복원하기 위해 스택을 팝

왜 수신자 저장 레지스터를 엉망으로 만드는가? 왜 이렇게하지 않습니까? 더 짧고 간단하며 아마도 더 빠를 것 같습니다.

  • x스택으로 밀어
  • 요구 foo
  • x스택에서 반환 값 레지스터로 팝

내 어셈블리가 잘못 되었습니까? 여분의 레지스터를 엉망으로 만드는 것보다 덜 효율적입니까? 두 가지 모두에 대한 답변이 "아니오"라면 GCC 나 Clang이 왜 그렇게하지 않습니까?

Godbolt 링크 .


편집 : 변수가 의미있게 사용 된 경우에도 발생하는 것을 보여주는 간단한 예가 있습니다.

long foo(long);

long bar(long x) {
    return foo(x * x) - x;
}

나는 이것을 얻는다 :

bar:
        push    rbx
        mov     rbx, rdi
        imul    rdi, rdi
        call    foo
        sub     rax, rbx
        pop     rbx
        ret

오히려 이것을 원합니다 :

bar:
        push    rdi
        imul    rdi, rdi
        call    foo
        pop     rdi
        sub     rax, rdi
        ret

이번에는 단 하나의 명령에 불과하지만 핵심 개념은 동일합니다.

Godbolt 링크 .


4
재미있는 누락 최적화.
fuz

1
전달 된 매개 변수가 사용된다는 가정 일 가능성이 높으므로 휘발성 레지스터를 저장하고 해당 매개 변수에 대한 후속 액세스가 레지스터에서 더 빠르므로 스택에없는 레지스터에 전달 된 매개 변수를 유지하려고합니다. x를 foo에 전달하면 이것을 볼 수 있습니다. 스택 프레임 설정의 일반적인 부분 일 것입니다.
old_timer

foo가 없으면 스택을 사용하지 않는다는 것을 알 수 있습니다. 그래서 누락 된 최적화이지만 누군가가 추가하고 함수를 분석해야하며 값이 사용되지 않고 해당 레지스터와 충돌이없는 경우 (일반적으로 거기에 있음) 입니다).
old_timer

팔 백엔드는 gcc 에서도이 작업을 수행합니다. 백엔드가
아님

clang 10 같은 이야기 (팔 백엔드).
old_timer

답변:


5

TL : DR :

  • 컴파일러 내부는 아마도이 최적화를 쉽게 찾도록 설정되지 않았으며 호출 사이의 큰 함수가 아닌 작은 함수에서만 유용 할 것입니다.
  • 큰 기능을 만들기위한 인라인은 대부분 더 나은 솔루션입니다.
  • fooRBX를 저장 / 복원하지 않으면 대기 시간 대 처리량 상충 관계가있을 수 있습니다 .

컴파일러는 복잡한 기계입니다. 그것들은 인간처럼 "똑똑한"것이 아니며, 가능한 모든 최적화를 찾는 고가의 알고리즘은 종종 추가 컴파일 시간에 비용이 들지 않습니다.

나는 이것을 2016 년에 push / pop을 사용하여 스필 / 리로드하여 GCC 버그 69986--Os로 가능한 더 작은 코드 로보고했다 ; GCC 개발자의 활동이나 답변이 없습니다. : /

약간 관련이 있음 : GCC 버그 70408-동일한 호출 유지 레지스터를 재사용하면 일부 코드가 더 작아집니다. 컴파일러 개발자는 GCC가 평가 순서를 따를 필요가 있기 때문에 최적화를 수행하려면 막대한 양의 작업이 필요하다고 말했습니다 foo(int)타겟 asm을 더 단순하게 만드는 것에 따라 두 번의 호출 중


경우 foo 저장하지 않습니다 / 복원 rbx자체를 온 여분의 저장 / 재 장전 대기 시간 대 처리량 (명령어 수) 사이의 트레이드 오프있다 x> RETVAL 의존성 체인 -.

컴파일러는 일반적으로 imul reg, reg, 10Skylake와 같은 일반적인 4 와이드 파이프 라인에서 대부분의 코드가 4 uops / clock보다 현저히 낮기 때문에 처리량 보다 지연 시간을 선호합니다 (예 : 3 사이클 지연 시간, 1 / 클럭 처리량 대신 2x LEA 사용 ). (더 많은 명령어 / uops는 ROB에서 더 많은 공간을 차지하므로 동일한 비 순차적 창에서 볼 수있는 범위를 훨씬 앞당길 수 있으며 실제로 4 개 미만의 uops / 시계 평균.)

경우 foo수행 푸시 / RBX 팝업 후 대기 시간 얻기 위해 많은이 아니다. 리턴 주소에서 코드 반입을 지연 ret시키는 ret잘못된 예측 또는 I- 캐시 미스가 없는 한 복원 직후 직후 대신 복원이 발생하는 것은 관련이 없습니다 .

사소하지 않은 대부분의 함수는 RBX를 저장 / 복원하기 때문에 RBX에 변수를 남겨두면 실제로 호출을 통해 레지스터에 실제로 머물렀다는 의미는 아닙니다. (통화 보존 레지스터 함수를 임의로 선택하는 것이 때때로 완화하는 것이 좋습니다.)


그래서 네 push rdi/ pop rax더 효율적인 것 경우,이 무엇에 따라, 아마 작은 잎 이외의 기능을위한 놓친 최적화입니다 foo수행하고 여분의 저장 / 재 장전 대기 시간 사이의 균형 x저장 / 발신자를 복원 할 대 더 지침 rbx.

stack-unwind 메타 데이터가 스택 슬롯으로 sub rsp, 8넘치거나 다시로드 하는 데 사용 된 것처럼 RSP에 대한 변경 사항을 여기에 표시 할 수 있습니다 x. (그러나 컴파일러가 사용하는, 하나이 최적화를 모르는 push예비 공간과 변수를 초기화합니다. 무엇 C / C ++ 컴파일러 지역 변수를 만드는 대신에 단지? ESP 한 번 증가시키기위한 푸시 팝 지침을 사용할 수 있습니다 . 그리고 일을 그 이상에 대한을 하나의 로컬 변수는 .eh_frame푸시 할 때마다 스택 포인터를 개별적으로 이동하기 때문에 스택 해제 메타 데이터 가 더 커지 므로 컴파일러는 푸시 / 팝을 사용하여 통화 보존 regs를 저장 / 복원하지 않습니다.)


컴파일러에게이 최적화를 찾도록 가르치는 것이 가치가 있다면 IDK

함수 내부의 한 번의 호출이 아닌 전체 함수에 대해 좋은 아이디어 일 수 있습니다. 그리고 내가 말했듯이, 그것은 fooRBX를 저장 / 복원 할 비관적 가정에 기반 합니다. (또는 x에서 반환 값까지의 대기 시간이 중요하지 않다면 처리량을 최적화하는 것이 중요합니다. 그러나 컴파일러는이를 알지 못하고 일반적으로 대기 시간에 맞게 최적화합니다).

함수 내부의 단일 함수 호출과 같이 많은 코드에서 비관적 가정을 시작하면 RBX가 저장 / 복원되지 않아 더 많은 사례를 활용할 수 있습니다.

루프 에서이 여분의 저장 / 복원 푸시 / 팝을 원하지 않고 루프 외부에서 RBX를 저장 / 복원하고 함수 호출을하는 루프에서 호출 유지 레지스터를 사용하십시오. 루프가 없어도 일반적인 경우 대부분의 함수는 여러 함수 호출을 수행합니다. 이 최적화 아이디어 x는 첫 번째와 마지막 직전의 호출 사이에서 실제로 사용하지 않는 경우에 적용 할 수 있습니다 . 그렇지 않으면 call하나의 팝 후에 하나의 팝을 수행 할 때 각각 16 바이트 스택 정렬을 유지하는 데 문제 가 다른 전화를하기 전에 전화하십시오.

컴파일러는 일반적으로 작은 기능에는 좋지 않습니다. 그러나 CPU에도 좋지 않습니다. 비 인라인 함수 호출은 컴파일러가 수신자의 내부를보고 평소보다 더 많은 가정을 할 수없는 한 최상의 최적화에 영향을 미칩니다 . 인라인이 아닌 함수 호출은 암시적인 메모리 장벽입니다. 호출자는 함수가 전역 적으로 액세스 가능한 데이터를 읽거나 쓸 수 있다고 가정해야하므로 이러한 모든 변수는 C 추상 머신과 동기화되어야합니다. (이스케이프 분석을 통해 주소가 함수를 이스케이프하지 않은 경우 호출을 통해 로컬에 레지스터를 유지할 수 있습니다.) 또한 컴파일러는 호출 클로버 된 레지스터가 모두 클로버되어 있다고 가정해야합니다. 이것은 호출 보존 된 XMM 레지스터가없는 x86-64 System V의 부동 소수점을 빨아들입니다.

작은 기능 bar()은 발신자에게 인라인하는 것이 좋습니다. 컴파일 -flto은 대부분의 경우 파일 경계에서도 발생할 수 있습니다. (함수 포인터와 공유 라이브러리 경계는 이것을 물리 칠 수 있습니다.)


컴파일러가 이러한 최적화를 시도하지 않은 이유 중 하나 는 컴파일러 내부 에서 호출 스택을 저장하는 방법을 알고있는 일반 스택과 레지스터 할당 코드와는 다른 컴파일러 코드가 필요 하다는 것입니다. 등록하고 사용하십시오.

즉, 구현하는 데 많은 작업이 필요하고 유지 관리해야 할 코드가 많을 것입니다.이 작업에 대해 너무 열광적 인 경우 코드 가 더 나빠질 수 있습니다.

또한 (희망적으로) 중요하지 않다는 것도; 중요한 경우 bar발신자에게 인라인하거나에 인라인 foo해야 bar합니다. 이것은 다른 많은이없는 한 괜찮 bar-like 기능과 foo큰, 그리고 어떤 이유로 그들은하지 인라인 자신의 발신자로 할 수 있습니다.


왜 일부 컴파일러가 그런 식으로 코드를 번역하는지, 왜 번역에서 오류가 아닌지 더 잘 사용할 수 있는지 묻는 질문은 확실하지 않습니다. 예를 들어, clang이 왜 그렇게 이상한 (최적화되지 않은) gcc, icc 및 심지어 msvc와 비교 하여이 루프를 처리
했는지 묻습니다.

1
@ RbMm : 당신의 요점을 이해하지 못합니다. 그것은이 질문에 관한 것과 관련이없는 clang에 대해 완전히 분리 된 누락 된 최적화처럼 보입니다. 누락 된 최적화 버그가 존재하며 대부분의 경우 수정해야합니다. 계속해서 bugs.llvm.org에보고하십시오
Peter Cordes

예, 내 코드 예제는 원래 질문과 관련이 없습니다. 이상한 (내 외모) 번역 (단일 clang 컴파일러에만 해당)의 또 다른 예입니다. 어쨌든 결과 asm 코드는 정확합니다. 최고가 아니며 eveen도 네이티브가 아님 gcc / icc / msvc
RbMm
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.