使用FPU和C内联汇编
我写了一个像这样的矢量结构:
struct vector { float x1, x2, x3, x4; };
然后我创建了一个函数,它使用向量使用内联汇编执行一些操作:
struct vector *adding(const struct vector v1[], const struct vector v2[], int size) { struct vector vec[size]; int i; for(i = 0; i < size; i++) { asm( "FLDL %4 \n" //v1.x1 "FADDL %8 \n" //v2.x1 "FSTL %0 \n" "FLDL %5 \n" //v1.x2 "FADDL %9 \n" //v2.x2 "FSTL %1 \n" "FLDL %6 \n" //v1.x3 "FADDL %10 \n" //v2.x3 "FSTL %2 \n" "FLDL %7 \n" //v1.x4 "FADDL %11 \n" //v2.x4 "FSTL %3 \n" :"=m"(vec[i].x1), "=m"(vec[i].x2), "=m"(vec[i].x3), "=m"(vec[i].x4) //wyjscie :"g"(&v1[i].x1), "g"(&v1[i].x2), "g"(&v1[i].x3), "g"(&v1[i].x4), "g"(&v2[i].x1), "g"(&v2[i].x2), "g"(&v2[i].x3), "g"(&v2[i].x4) //wejscie : ); } return vec; }
一切看起来都不错,但是当我尝试使用GCC编译时,我得到了以下错误:
错误:’fadd’的操作数类型不匹配
错误:’fld’的指令后缀无效
在XCode的OS / X上一切正常。 这段代码有什么问题?
编码问题
我不打算提高效率(如果处理器支持它,我会使用SSE / SIMD)。 由于这部分任务是使用FPU堆栈,因此这里有一些问题:
您的函数声明了一个基于本地堆栈的变量:
struct vector vec[size];
问题是你的函数返回一个vector *
,你这样做:
return vec;
这真是太糟了。 在函数返回之后和调用者使用数据之前,基于堆栈的变量可能会被破坏。 一种替代方法是在堆上而不是堆栈上分配内存。 你可以替换struct vector vec[size];
有:
struct vector *vec = malloc(sizeof(struct vector)*size);
这将为一个size
为vector
的数组分配足够的空间。 调用函数的人必须使用free
来在完成时从堆中释放内存。
您的vector
结构使用float
,而不是double
。 FLDL , FADDL , FSTL指令都在双(64位浮点数)上运行。 当与内存操作数一起使用时,这些指令中的每一个都将加载和存储64位。 这将导致向FPU堆栈加载/存储错误的值。 您应该使用FLDS , FADDS , FSTS来操作32位浮点数。
在汇编程序模板中,您在输入上使用g
约束。 这意味着编译器可以自由使用任何通用寄存器,内存操作数或立即值。 FLDS , FADDS , FSTS不采用立即值或通用寄存器(非FPU寄存器),因此如果编译器尝试这样做,则可能会产生类似于Error: Operand type mismatch for xxxx
。
由于这些指令理解内存引用使用m
而不是g
约束。 您需要从输入操作数中删除&
(和号),因为m
意味着它将处理变量/ C表达式的内存地址。
完成后,您不会从FPU堆栈中弹出值。 具有单个操作数的FST将堆栈顶部的值复制到目标。 堆栈上的值仍然存在。 您应该存储它并使用FSTP指令将其弹出。 当汇编程序模板结束时,您希望FPU堆栈为空。 FPU堆栈非常有限,只有8个可用插槽。 如果模板完成时FPU堆栈未清除,则表示您在后续调用中存在FPU堆栈溢出的风险。 由于每次调用都会在堆栈上留下4个值,因此调用第三次adding
的函数会失败。
为了简化代码,我建议使用typedef
来定义向量。 以这种方式定义您的结构:
typedef struct { float x1, x2, x3, x4; } vector;
对struct vector
所有引用都可以简单地成为vector
。
考虑到所有这些因素,您的代码可能如下所示:
typedef struct { float x1, x2, x3, x4; } vector; vector *adding(const vector v1[], const vector v2[], int size) { vector *vec = malloc(sizeof(vector)*size); int i; for(i = 0; i < size; i++) { __asm__( "FLDS %4 \n" //v1.x1 "FADDS %8 \n" //v2.x1 "FSTPS %0 \n" "FLDS %5 \n" //v1.x2 "FADDS %9 \n" //v2.x2 "FSTPS %1 \n" "FLDS %6 \n" //v1->x3 "FADDS %10 \n" //v2->x3 "FSTPS %2 \n" "FLDS %7 \n" //v1->x4 "FADDS %11 \n" //v2->x4 "FSTPS %3 \n" :"=m"(vec[i].x1), "=m"(vec[i].x2), "=m"(vec[i].x3), "=m"(vec[i].x4) :"m"(v1[i].x1), "m"(v1[i].x2), "m"(v1[i].x3), "m"(v1[i].x4), "m"(v2[i].x1), "m"(v2[i].x2), "m"(v2[i].x3), "m"(v2[i].x4) : ); } return vec; }
替代方案
我不知道赋值的参数,但是如果它是让你使用GCC扩展汇编程序模板来手动对带有FPU指令的向量进行操作那么你可以用4 float
组定义向量。 使用嵌套循环处理向量的每个元素,将每个元素独立地传递给汇编器模板以便一起添加。
将vector
定义为:
typedef struct { float x[4]; } vector;
function如下:
vector *adding(const vector v1[], const vector v2[], int size) { int i, e; vector *vec = malloc(sizeof(vector)*size); for(i = 0; i < size; i++) for (e = 0; e < 4; e++) { __asm__( "FADDPS\n" :"=t"(vec[i].x[e]) :"0"(v1[i].x[e]), "u"(v2[i].x[e]) ); } return vec; }
这在操作数上使用i386机器约束 t
和u
。 我们允许GCC通过FPU堆栈的前两个插槽传递它们,而不是传递内存地址。 t
和u
定义为:
t Top of 80387 floating-point stack (%st(0)). u Second from top of 80387 floating-point stack (%st(1)).
FADDP的无操作数forms是这样的:
将ST(0)添加到ST(1),将结果存储到ST(1),然后弹出寄存器堆栈
我们将两个值传递给堆栈顶部并执行操作, 只将结果保留在ST(0)中 。 然后我们可以获取汇编程序模板来复制堆栈顶部的值并自动将其弹出。
我们可以使用=t
的输出操作数来指定我们想要移动的值来自FPU堆栈的顶部。 =t
也将为我们弹出(如果需要)FPU堆栈顶部的值。 我们也可以使用堆栈顶部作为输入值! 如果输出操作数是%0,我们可以将它作为输入操作数引用,约束为0
(这意味着使用与操作数0相同的约束)。 第二个向量值将使用u
约束,因此它作为第二个FPU堆栈元素传递( ST(1) )
可能允许GCC优化其生成的代码的略微改进是在第一个输入操作数上使用%
修饰符 。 %
修饰符记录为:
声明该操作数和以下操作数的可交换指令。 这意味着如果这是使所有操作数适合约束的最便宜方式,则编译器可以交换两个操作数。 '%'适用于所有替代项,必须作为约束中的第一个字符出现。 只有只读操作数才能使用'%'。
因为x + y和y + x产生相同的结果,我们可以告诉编译器它可以将标记为%
的操作数与在模板中紧接着之后定义的操作数交换。 "0"(v1[i].x[e])
可以更改为"%0"(v1[i].x[e])
缺点 :我们已将汇编程序模板中的代码缩减为单个指令,并且我们已使用该模板完成大部分工作设置并将其拆除。 问题在于,如果向量可能是内存绑定的,那么我们在FPU寄存器和内存之间进行传输,并返回比我们想要的更多次。 正如我们在Godbolt输出中看到的那样,生成的代码可能效率不高。
我们可以通过将原始代码中的想法应用到模板来强制使用内存。 此代码可能会产生更合理的结果:
vector *adding(const vector v1[], const vector v2[], int size) { int i, e; vector *vec = malloc(sizeof(vector)*size); for(i = 0; i < size; i++) for (e = 0; e < 4; e++) { __asm__( "FADDS %2\n" :"=&t"(vec[i].x[e]) :"0"(v1[i].x[e]), "m"(v2[i].x[e]) ); } return vec; }
注意 :在这种情况下我删除了%
修饰符。 理论上它应该可行,但是当针对x86-64时, GCC似乎发出效率较低的代码( CLANG似乎没问题)。 我不确定这是不是一个bug; 我的理解是否缺乏这个操作员应该如何工作; 或者有一个我不明白的优化 。 直到我仔细观察它,我将它留下来生成我希望看到的代码。
在最后一个例子中,我们强制FADDS指令操作内存操作数。 Godbolt输出相当清晰,循环本身看起来像:
.L3: flds (%rdi) # MEM[base: _51, offset: 0B] addq $16, %rdi #, ivtmp.6 addq $16, %rcx #, ivtmp.8 FADDS (%rsi) # _31->x fstps -16(%rcx) # _28->x addq $16, %rsi #, ivtmp.9 flds -12(%rdi) # MEM[base: _51, offset: 4B] FADDS -12(%rsi) # _31->x fstps -12(%rcx) # _28->x flds -8(%rdi) # MEM[base: _51, offset: 8B] FADDS -8(%rsi) # _31->x fstps -8(%rcx) # _28->x flds -4(%rdi) # MEM[base: _51, offset: 12B] FADDS -4(%rsi) # _31->x fstps -4(%rcx) # _28->x cmpq %rdi, %rdx # ivtmp.6, D.2922 jne .L3 #,
在最后一个例子中, GCC展开了内环,只留下了外环。 编译器生成的代码在本质上与原始问题的汇编程序模板中手动生成的代码类似。