New Calling Convention for AMD64(=EM64T, x64) and IA32(=x86)
주의 사항
이 글은 예전 블로그에서 옮겨온 오래 된 글입니다. 현재 상황과는 다를 수 있으며, 잘못 된 정보가 있을 수 있습니다.
전 x86 시절에는 다양한 calling convention 이 있었습니다.
MS가 OS API 의 디폴트로 사용한 pascal 식 호출방식(__stdcall) 막상 언어로 파스칼을 쓰는 델파이가 파스칼식을 사용 하지 않고 사용하고 있던 fastcall, 그리고 c에서 사용하는 cdecl이 각각의 호출 방식은 각각 장단점이 있었는데, pascal 식은 바이너리의 크기가 작아진다, fastcall 은 인자 전달에 레지스터를 사용 하므로 속도에 유리하다, cdecl 은 최적화 단계에서 스택 push ,pop 을 생략할 수 있으므로, 최적화에 유리하고 가변인지를 지원 할 수 있다는 장점이 있었습니다.
그러나 바야흐로 x64 시대, 컴파일러를 설계하는 사람들은 각각의 장점을 통합한 새로운 호출 방식을 만들게 되었습니다. 그 이후 calling convention 을 지정하는 모든 키워드는 x64 컴파일시에 무시되며, 그런거 신경 안써도 되는 평화로운 세월이 이어져 왔습니다.
그러나 시간이 흘러 CPU는 클럭경쟁의 시대가 막이 내리고, SMID 와 멀티코어의 시대로 넘어 왔는데… 당연히 CPU 제조사들은 새 SIMD 명령어를 추가 하기 시작 했고, 그에 맞춰 새 명령어용 레지스터가 추가 되었습니다. 그 시점에서 컴파일러 개발자들은 이렇게 생각 햇나 봅니다 “어? 레지스터가 놀고있네?” 라고. 그리하여 VS2013 이후부터는 __vectorcall 이라는 키워드가 추가되어, 더이상 x64 바이너리에서도 calling convention 을 무시 할 수 없게 되었습니다.
간단한 예제를 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <cstdio>
#include <intrin.h>
__m256i func(double d1, double d2, double d3, double d4, __m256i f)
{
printf("%f %f %f %f\n", d1, d2, d3, d4);
printf("%lld %lld %lld %lld\n",
f.m256i_u64[0], f.m256i_u64[1], f.m256i_u64[2], f.m256i_u64[3]);
return f;
}
int __cdecl main()
{
__m256i f;
for (int i = 0; i < 4; ++i)
{
f.m256i_u64[i] = i;
}
f = func(0.1, 0.2, 0.3, 0.4, f);
return 0;
}
이코드를 빌드해서 실행 해 보면 다음과 같은 코드를 볼 수 있습니다.
그리고 함수의 프로토타입에 __vectorcall 을 추가하여
1
__m256i __vectorcall func(double d1, double d2, double d3, double d4, __m256i f)
와 같이 만든 후 다시 빌드 해 보겠습니다
차이가 보이시나요? 첫 줄에서 ymm4 라는 avx 레지스터가 추가적으로 함수 전달에 쓰이는 것을 볼 수 있습니다. 매번 __vectorcall 을 붙이기 싫다면
옵션에서 조정 하거나, /Gv 플래그를 추가하여 사용 할 수도 있습니다. 단 /Gv 플래그를 켤 경우, main 은 항상 __cdecl 이어야 한다는 조건이 있으므로, 위 코드처럼 __cdecl 을 붙여주지 않으면 경고가 발생 할 것이니 조심하세요.