이 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이 왜 그렇게하지 않습니까?
편집 : 변수가 의미있게 사용 된 경우에도 발생하는 것을 보여주는 간단한 예가 있습니다.
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
이번에는 단 하나의 명령에 불과하지만 핵심 개념은 동일합니다.
답변
TL : DR :
- 컴파일러 내부는 아마도이 최적화를 쉽게 찾도록 설정되지 않았으며 호출 사이의 큰 함수가 아닌 작은 함수에서만 유용 할 것입니다.
- 큰 기능을 만들기위한 인라인은 대부분 더 나은 솔루션입니다.
foo
RBX를 저장 / 복원하지 않으면 대기 시간 대 처리량 상충 관계가있을 수 있습니다 .
컴파일러는 복잡한 기계입니다. 그것들은 인간처럼 “똑똑한”것이 아니며, 가능한 모든 최적화를 찾는 고가의 알고리즘은 종종 추가 컴파일 시간에 비용이 들지 않습니다.
나는 이것을 2016 년에 push / pop을 사용하여 스필 / 리로드하여 GCC 버그 69986–Os로 가능한 더 작은 코드 로보고했다 ; GCC 개발자의 활동이나 답변이 없습니다. : /
약간 관련이 있음 : GCC 버그 70408-동일한 호출 유지 레지스터를 재사용하면 일부 코드가 더 작아집니다. 컴파일러 개발자는 GCC가 평가 순서를 따를 필요가 있기 때문에 최적화를 수행하려면 막대한 양의 작업이 필요하다고 말했습니다 foo(int)
타겟 asm을 더 단순하게 만드는 것에 따라 두 번의 호출 중
경우 foo
저장하지 않습니다 / 복원 rbx
자체를 온 여분의 저장 / 재 장전 대기 시간 대 처리량 (명령어 수) 사이의 트레이드 오프있다 x
> RETVAL 의존성 체인 -.
컴파일러는 일반적으로 imul reg, reg, 10
Skylake와 같은 일반적인 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
함수 내부의 한 번의 호출이 아닌 전체 함수에 대해 좋은 아이디어 일 수 있습니다. 그리고 내가 말했듯이, 그것은 foo
RBX를 저장 / 복원 할 비관적 가정에 기반 합니다. (또는 x에서 반환 값까지의 대기 시간이 중요하지 않다면 처리량을 최적화하는 것이 중요합니다. 그러나 컴파일러는이를 알지 못하고 일반적으로 대기 시간에 맞게 최적화합니다).
함수 내부의 단일 함수 호출과 같이 많은 코드에서 비관적 가정을 시작하면 RBX가 저장 / 복원되지 않아 더 많은 사례를 활용할 수 있습니다.
루프 에서이 여분의 저장 / 복원 푸시 / 팝을 원하지 않고 루프 외부에서 RBX를 저장 / 복원하고 함수 호출을하는 루프에서 호출 유지 레지스터를 사용하십시오. 루프가 없어도 일반적인 경우 대부분의 함수는 여러 함수 호출을 수행합니다. 이 최적화 아이디어 x
는 첫 번째와 마지막 직전의 호출 사이에서 실제로 사용하지 않는 경우에 적용 할 수 있습니다 . 그렇지 않으면 call
하나의 팝 후에 하나의 팝을 수행 할 때 각각 16 바이트 스택 정렬을 유지하는 데 문제 가 다른 전화를하기 전에 전화하십시오.
컴파일러는 일반적으로 작은 기능에는 좋지 않습니다. 그러나 CPU에도 좋지 않습니다. 비 인라인 함수 호출은 컴파일러가 수신자의 내부를보고 평소보다 더 많은 가정을 할 수없는 한 최상의 최적화에 영향을 미칩니다 . 인라인이 아닌 함수 호출은 암시적인 메모리 장벽입니다. 호출자는 함수가 전역 적으로 액세스 가능한 데이터를 읽거나 쓸 수 있다고 가정해야하므로 이러한 모든 변수는 C 추상 머신과 동기화되어야합니다. (이스케이프 분석을 통해 주소가 함수를 이스케이프하지 않은 경우 호출을 통해 로컬에 레지스터를 유지할 수 있습니다.) 또한 컴파일러는 호출 클로버 된 레지스터가 모두 클로버되어 있다고 가정해야합니다. 이것은 호출 보존 된 XMM 레지스터가없는 x86-64 System V의 부동 소수점을 빨아들입니다.
작은 기능 bar()
은 발신자에게 인라인하는 것이 좋습니다. 컴파일 -flto
은 대부분의 경우 파일 경계에서도 발생할 수 있습니다. (함수 포인터와 공유 라이브러리 경계는 이것을 물리 칠 수 있습니다.)
컴파일러가 이러한 최적화를 시도하지 않은 이유 중 하나 는 컴파일러 내부 에서 호출 스택을 저장하는 방법을 알고있는 일반 스택과 레지스터 할당 코드와는 다른 컴파일러 코드가 필요 하다는 것입니다. 등록하고 사용하십시오.
즉, 구현하는 데 많은 작업이 필요하고 유지 관리해야 할 코드가 많을 것입니다.이 작업에 대해 너무 열광적 인 경우 코드 가 더 나빠질 수 있습니다.
또한 (희망적으로) 중요하지 않다는 것도; 중요한 경우 bar
발신자에게 인라인하거나에 인라인 foo
해야 bar
합니다. 이것은 다른 많은이없는 한 괜찮 bar
-like 기능과 foo
큰, 그리고 어떤 이유로 그들은하지 인라인 자신의 발신자로 할 수 있습니다.