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