LLVMの呼び出しプロトコルとメモリの位置合わせ

3299 ワード

LLVMの呼び出しプロトコルとメモリの位置合わせ
ある言語が他の言語と対話するAPIとABI(ApplicationBinary Interface,バイナリインタフェース)を設計する際、呼び出しプロトコルとメモリの位置合わせは2つの避けられない問題である.
この文書では、LLVM上で適切なメモリアライメントおよび呼び出しプロトコルのコードを生成する方法について説明します.
ここでは便宜上、標準化のため、LLVMを適用する言語のExtendingとEmbeddingの両方のオブジェクトがCであると仮定する.
呼び出しプロトコル
まず呼び出しプロトコルについて議論します.呼び出しプロトコルは、呼び出し元と被呼び出し元がバイナリ/アセンブリレベルで互換性があることを保証するために使用されます.適切な呼び出しプロトコルは、次のコードを構築するのに役立ちます.
// 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);

一般に呼び出しプロトコルには、パラメータ転送と戻り値転送、スタックバランスの3つの部分が含まれます.x 86プラットフォーム上のC/C++コンパイラで一般的な呼び出しプロトコルはcdecl,fastcall,stdcallである.具体的なプロトコルの内容はMSDNを参照してください.
C++には、オブジェクトのメンバー関数を呼び出すための特別な呼び出しプロトコルthiscallもあります.しかし、このような呼び出しプロトコルの異なるプラットフォームでは、異なるコンパイラ実装が異なり、書面基準も事実基準もなく、virtual callなどの複雑な状況が存在し、言語間呼び出しには適していない.
x 64プラットフォームでは、windowsとlinuxの2つの呼び出しプロトコルがあります.
まずx 86を見てみましょう.x 86はcdeclおよびfastcallにおいてプラットフォーム間で標準的であるため、LLVMのサポートは比較的完全である.プログラムはFunctionの作成時にCall Conventionを指定すればよい.
しかし、x 64については、LLVMのサポートはそれほど完璧ではありません.Windowsを例にとると、windowsのx 64呼び出しプロトコルは、rcx、rdx、r 8、r 9レジスタで64 bit未満の4つのパラメータを渡し、残りのパラメータをスタックに置くことを要求する.パラメータが64 bitより大きい場合は、ポインタを渡す必要があります.浮動小数点はxmm 0-3を用いて伝達される.しかし、LLVMの場合、パラメータが64 bitより大きいと、ポインタではなくオブジェクト全体がスタックに押し付けられて伝達されます.したがって、x 64に遭遇した場合、API部分の呼び出しプロトコルを慎重に処理する必要がある.
ここでは,64 bitを超えるすべての構造体をポインタ(またはコピー後にポインタ)に処理して伝達する必要がある.
同時に、LLVMは、readonlyとbyvalの2つのパラメータ属性(Attribute)を提供し、パラメータの値の意味を保証します.前者は、入力されたポインタが指す値が変更されないことを意味し、後者は入力されたポインタをメモリコピーし、書き込み値が関数から伝達されないことを保証します.(値コピーと同様).これにより、LLVMによって生成された関数がMSVCによって生成されたx 64コードで正しく呼び出されます.
メモリの位置合わせ
モバイルプラットフォームのアーキテクチャに比べて、x 86のメモリ位置合わせの条件はかなり緩やかです.ほとんどの命令はメモリの位置合わせに特に要求されません.一部のSIMDの命令だけがメモリの位置合わせを制限します.例えばmovapsです.
バックエンドのSIMDコードの生成を容易にするために、LLVMはvectorのようなvectorタイプを提供する.コード生成時にはvectorが最も可能性のあるSIMDタイプにコンパイルされます.従ってx 86プラットフォームではvectorは__に類似するように処理されるm 128のタイプで、より長いvectorは複数の__に分割されるm 128タイプ.
これは実際には、すべてのvectorが16 Bytes整列の原則に従うべきであることを意味します.
我々のニーズを考慮するとstruct{float[3];このような構造は、vectorとして表すことができれば、shuffleなどの数学演算に明らかに適しており、要素ごとのadd,sub,mul、同時にLLVM命令の選択もより柔軟である.しかし、この構造体には、16バイトの整列と16バイトのサイズ(movupsとmovapsはともに1回に16バイトを取る)の2つの条件が満たされていないことが明らかになった.これにより、境界下の読み書きのメモリが境界を越えてしまう.そのため、残念ながら、これらのデータはstruct{float,float,float}として表さなければならない.読み取る際にも、正しい命令:movssが生成される.
では、一般的な非整列vec 4にvectorを適用するのはいかがでしょうか.
答えは、難しいです.LLVMでは、非整列時のvectorの応用を設計する際にあまり考慮していません.loadとstoreはalignmentを指定してmovupsなどの非整列メモリ操作を生成することができ、確かに効果的であるが、コード最適化、一時アクセスなどの特性の存在により、一部の非loadとstoreのメモリ操作は依然として整列を要求している(例えばaddaps xmm、[addr]).この場合も、非整列のデータに対してメモリ整列の命令が生成される可能性があります.
そこで総合的に比較すると,SASLはAPIインタフェース上でstruct{float x,y,z,wを用いた.このようなABIはデータを表し、コード生成時にstructのデータをvectorに変換してから他の操作を実行し、ABIとSIMDを両立させる.同時にIntrinsicでは、ホストに露出しないため、できるだけVectorを使用し、LLVMの最適化を容易にします.