岁月流转,往昔空明

C++博客 首页 新随笔 联系 聚合 管理
  118 Posts :: 3 Stories :: 413 Comments :: 0 Trackbacks

在设计一门语言与其他语言交互的API与ABI(Application Binary Interface,二进制接口)时,调用协议和内存对齐是两个无从回避的问题。

本文将讨论如何在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);

一般来说调用协议包括参数传递和返回值传递和堆栈平衡三个部分。在x86平台上的C/C++编译器中常见的调用协议有cdecl, fastcall和stdcall。具体的协议内容请参见MSDN。

在C++中还有一类特殊的调用协议thiscall,用于调用对象的成员函数。但是这一类调用协议不同的平台,不同的编译器实现皆有不同,既无书面标准,也无事实标准,再加上virtual call等复杂的情况存在,并不适合用于做跨语言的调用。

对于x64平台而言,在windows下和linux下分别有两种调用协议。

先来看x86。由于x86在cdecl和fastcall上是有着跨平台的标准的,因此LLVM对它的支持是比较完整的。程序只要在创建Function的时候指定Call Convention即可。

但是对于x64,LLVM的支持便不是那么完善。以windows为例,windows的x64调用协议要求以rcx,rdx,r8,r9寄存器传递前四个不大于64bit的参数,其余参数放在栈上。如果参数大于64bit,则要求传递它的指针。浮点使用xmm0-3来传递。但是对于LLVM而言,一旦参数大于64bit,它便会将整个对象而不是指针压到栈上传递。因此在遇到x64时,需要小心处理API部分的调用协议。

在这里,我们需要将所有超过64bit的结构体处理成指针(或者拷贝后处理成指针)传递。

同时,LLVM提供了readonly和byval两个参数属性(Attribute)来确保参数的值语义。前者意味着传入的指针所指向的值是不被修改的,(类似于T const*),而后者会对传入的指针做一份内存拷贝,确保写值不被传递出函数(类似于值拷贝)。这样,LLVM生成的函数便可以MSVC生成的x64代码正确调用了。

内存对齐

与移动平台的体系结构相比,x86对内存对齐的条件算是相当宽松的了。大部分的指令对内存对齐基本上是没有特殊要求的。只有一些SIMD的指令会对内存对齐有所限定,例如movaps。

为了方便后端生成SIMD代码,LLVM提供了vector类型,例如vector<float, 1>。在代码生成的时候,vector会编译成最有可能的SIMD类型。因此在x86平台上,vector<float, 1-4>都被处理成类似于__m128的类型,更长的vector则被拆分成多个__m128类型。

这实际上意味着,所有的vector都应该遵循16Bytes对齐的原则。

考虑到我们的需求,类似于struct{ float[3]; }这样的结构,如果能表示为vector<float, 3>显然适合一些数学运算,例如shuffle,逐元素的add,sub,mul,同时LLVM指令的选择也更加灵活。但是显然,这个结构体有两个条件是不满足的:16字节对齐和16字节的大小(movups和movaps都是一次取16字节)。这会造成边界下读写的内存越界。因此非常可惜,这些数据必须表示为struct{ float ,float, float }。在读取的时候,也会生成正确的指令:movss。

那么,对于一般的非对齐的vec4应用vector<float,4>行不行呢?

答案是,很困难。对于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,由于并不暴露给Host,所以它们仍然尽可能使用Vector,便于LLVM进行优化。

posted on 2011-08-17 13:58 空明流转 阅读(3307) 评论(3)  编辑 收藏 引用

评论

# re: LLVM的调用协议与内存对齐 2011-08-17 16:38 陈梓瀚(vczh)
好复杂啊  回复  更多评论
  

# re: LLVM的调用协议与内存对齐 2011-08-18 00:36 ooseven
期待空转兄的大作,希望做成一个系列。  回复  更多评论
  

# re: LLVM的调用协议与内存对齐 2014-04-03 09:56 往往
在建立扎实收入之前的那种开支需求的一种保障在生活,保障在房租,保障在零用之间建立起来的三个基本的扎实收入的过程当中所需要的一种付出其中的基本的一个事实的努力的参考的基本含量在于确定起来的是基于一个现实的暂时的扎实的基础收入建立起来对于沟通模式畅通的一个局面上面建立起来的扎实收入的基本面,这样的一个基本面不是说自己具备的这样的一种自我意识的过程当中来寻找这样的一个扎实基础收入的可能,而是一定需要建立这样扎实收入的基本面,在这个之前付出就需要更加斟酌的保持其中的一个安全的自我层面来考量。  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理