在設(shè)計(jì)一門語言與其他語言交互的API與ABI(Application Binary Interface,二進(jìn)制接口)時(shí),調(diào)用協(xié)議和內(nèi)存對齊是兩個(gè)無從回避的問題。
本文將討論如何在LLVM上生成正確的內(nèi)存對齊和調(diào)用協(xié)議的代碼。
在這里為了方便和標(biāo)準(zhǔn)起見,假定應(yīng)用LLVM的語言的Extending和Embedding的對象都是C。
調(diào)用協(xié)議
先來討論調(diào)用協(xié)議。調(diào)用協(xié)議用于保證調(diào)用方和被調(diào)用方在二進(jìn)制/匯編一級上是相容的。合適的調(diào)用協(xié)議可以幫助構(gòu)造出以下代碼:
// Callee Signature of LLVM code
void __cdecl foo( int a, float b, float4 c);
// C caller
typedef void (__cdecl* fn_ptr)(int, float, float4)
fn_ptr p = static_cast<fn_ptr>( get_jit_function("foo") );
p(1, 1.0, vec);
|
一般來說調(diào)用協(xié)議包括參數(shù)傳遞和返回值傳遞和堆棧平衡三個(gè)部分。在x86平臺上的C/C++編譯器中常見的調(diào)用協(xié)議有cdecl, fastcall和stdcall。具體的協(xié)議內(nèi)容請參見MSDN。
在C++中還有一類特殊的調(diào)用協(xié)議thiscall,用于調(diào)用對象的成員函數(shù)。但是這一類調(diào)用協(xié)議不同的平臺,不同的編譯器實(shí)現(xiàn)皆有不同,既無書面標(biāo)準(zhǔn),也無事實(shí)標(biāo)準(zhǔn),再加上virtual call等復(fù)雜的情況存在,并不適合用于做跨語言的調(diào)用。
對于x64平臺而言,在windows下和linux下分別有兩種調(diào)用協(xié)議。
先來看x86。由于x86在cdecl和fastcall上是有著跨平臺的標(biāo)準(zhǔn)的,因此LLVM對它的支持是比較完整的。程序只要在創(chuàng)建Function的時(shí)候指定Call Convention即可。
但是對于x64,LLVM的支持便不是那么完善。以windows為例,windows的x64調(diào)用協(xié)議要求以rcx,rdx,r8,r9寄存器傳遞前四個(gè)不大于64bit的參數(shù),其余參數(shù)放在棧上。如果參數(shù)大于64bit,則要求傳遞它的指針。浮點(diǎn)使用xmm0-3來傳遞。但是對于LLVM而言,一旦參數(shù)大于64bit,它便會將整個(gè)對象而不是指針壓到棧上傳遞。因此在遇到x64時(shí),需要小心處理API部分的調(diào)用協(xié)議。
在這里,我們需要將所有超過64bit的結(jié)構(gòu)體處理成指針(或者拷貝后處理成指針)傳遞。
同時(shí),LLVM提供了readonly和byval兩個(gè)參數(shù)屬性(Attribute)來確保參數(shù)的值語義。前者意味著傳入的指針?biāo)赶虻闹凳遣槐恍薷牡?,(類似于T const*),而后者會對傳入的指針做一份內(nèi)存拷貝,確保寫值不被傳遞出函數(shù)(類似于值拷貝)。這樣,LLVM生成的函數(shù)便可以MSVC生成的x64代碼正確調(diào)用了。
內(nèi)存對齊
與移動(dòng)平臺的體系結(jié)構(gòu)相比,x86對內(nèi)存對齊的條件算是相當(dāng)寬松的了。大部分的指令對內(nèi)存對齊基本上是沒有特殊要求的。只有一些SIMD的指令會對內(nèi)存對齊有所限定,例如movaps。
為了方便后端生成SIMD代碼,LLVM提供了vector類型,例如vector<float, 1>。在代碼生成的時(shí)候,vector會編譯成最有可能的SIMD類型。因此在x86平臺上,vector<float, 1-4>都被處理成類似于__m128的類型,更長的vector則被拆分成多個(gè)__m128類型。
這實(shí)際上意味著,所有的vector都應(yīng)該遵循16Bytes對齊的原則。
考慮到我們的需求,類似于struct{ float[3]; }這樣的結(jié)構(gòu),如果能表示為vector<float, 3>顯然適合一些數(shù)學(xué)運(yùn)算,例如shuffle,逐元素的add,sub,mul,同時(shí)LLVM指令的選擇也更加靈活。但是顯然,這個(gè)結(jié)構(gòu)體有兩個(gè)條件是不滿足的:16字節(jié)對齊和16字節(jié)的大?。╩ovups和movaps都是一次取16字節(jié))。這會造成邊界下讀寫的內(nèi)存越界。因此非常可惜,這些數(shù)據(jù)必須表示為struct{ float ,float, float }。在讀取的時(shí)候,也會生成正確的指令:movss。
那么,對于一般的非對齊的vec4應(yīng)用vector<float,4>行不行呢?
答案是,很困難。對于LLVM而言,他們在設(shè)計(jì)的時(shí)候就沒有過多的考慮vector在非對齊時(shí)候的應(yīng)用。盡管load和store都能夠指定alignment以生成非對齊的內(nèi)存操作(例如movups)并且確實(shí)會起效,但是由于代碼優(yōu)化、臨時(shí)存取等特性的存在,導(dǎo)致一些非load和store的內(nèi)存操作仍然是要求對齊的(例如生成了addaps xmm, [addr])。此時(shí)仍然有可能為非對齊的數(shù)據(jù)生成了內(nèi)存對齊的指令。
因此綜合權(quán)衡,SASL在API界面上使用了struct{float x,y,z,w;} 這樣的ABI來表示數(shù)據(jù),在代碼生成時(shí),會首先將struct的數(shù)據(jù)轉(zhuǎn)換成vector,然后再執(zhí)行其它的操作,兼顧ABI與SIMD;同時(shí)對于Intrinsic,由于并不暴露給Host,所以它們?nèi)匀槐M可能使用Vector,便于LLVM進(jìn)行優(yōu)化。