将 FPU 与 C 内联汇编一起使用

Using FPU with C inline assembly

我写了一个这样的向量结构:

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 编译它时,我得到了这些错误:

Error: Operand type mismatch for 'fadd'

Error: Invalid instruction suffix for 'fld'

在 OS/X 在 XCode 一切正常。这段代码有什么问题?

编码问题

我不打算提高效率(如果处理器支持,我会使用 SSE/SIMD)。由于这部分作业是使用 FPU 堆栈,因此我有一些担忧:

您的函数声明了一个基于局部堆栈的变量:

struct vector vec[size];

问题是你的函数 returns a vector * 而你这样做:

return vec;

这很糟糕。基于堆栈的变量可能会在函数 returns 之后和调用者使用数据之前被破坏。一种替代方法是在堆上而不是堆栈上分配内存。您可以将 struct vector vec[size]; 替换为:

struct vector *vec = malloc(sizeof(struct vector)*size);

这将为 sizevector 的数组分配足够的 space。调用您的函数的人在完成后必须使用 free 从堆中释放内存。


您的 vector 结构使用 float,而不是 double。指令 FLDLFADDLFSTL 都对双精度(64 位浮点数)进行操作。当与内存操作数一起使用时,这些指令中的每一条都将加载和存储 64 位。这将导致 FPU 堆栈的错误值为 loaded/stored to/from。您应该使用 FLDSFADDSFSTS 对 32 位浮点数进行操作。


在汇编程序模板中,您对输入使用 g 约束。这意味着编译器可以自由使用任何通用寄存器、内存操作数或立即值。 FLDSFADDSFSTS不取立即数或通用寄存器(非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;

函数为:

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 machine constraints tu。我们允许 GCC 通过 FPU 堆栈顶部的两个插槽传递它们,而不是传递内存地址。 tu 定义为:

t
Top of 80387 floating-point stack (%st(0)).

u
Second from top of 80387 floating-point stack (%st(1)). 

FADDP 的无操作数形式是这样做的:

Add ST(0) to ST(1), store result in ST(1), and pop the register stack

我们将要添加的两个值传递到堆栈的顶部并执行一个操作,将 ONLY 结果留在 ST(0).然后我们可以让汇编器模板复制堆栈顶部的值并自动弹出它。

我们可以使用 =t 的输出操作数来指定我们要从 FPU 堆栈的顶部移动的值。 =t 还将为我们弹出(如果需要)FPU 堆栈顶部的值。我们也可以使用栈顶作为输入值!如果输出操作数是 %0,我们可以将其引用为具有约束 0 的输入操作数(这意味着使用与操作数 0 相同的约束)。第二个向量值将使用 u 约束,因此它作为第二个 FPU 堆栈元素传递 (ST(1))

可能允许 GCC 优化其生成的代码的轻微改进是在第一个输入操作数上使用 % modifier% 修饰符记录为:

Declares the instruction to be commutative for this operand and the following operand. This means that the compiler may interchange the two operands if that is the cheapest way to make all operands fit the constraints. ‘%’ applies to all alternatives and must appear as the first character in the constraint. Only read-only operands can use ‘%’.

因为 x+y 和 y+x 产生相同的结果,我们可以告诉编译器它可以将标有 % 的操作数与模板中紧随其后定义的操作数交换。 "0"(v1[i].x[e]) 可以更改为 "%0"(v1[i].x[e])

缺点:我们已将汇编程序模板中的代码减少为一条指令,并且我们已使用该模板完成大部分设置和撕裂工作它下来。问题是,如果向量可能会受内存限制,那么我们在 FPU 寄存器和内存之间传输和返回的次数比我们希望的要多。正如我们在 Godbolt output.

中看到的那样,生成的代码可能不是很有效

我们可以通过将您原始代码中的想法应用于模板来强制使用内存。此代码可能会产生更合理的结果:

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;
}

注意:在这种情况下,我删除了 % 修饰符。理论上它应该可以工作,但是 GCC 似乎在针对 x86-64 时发出效率较低的代码(CLANG 似乎没问题)。我不确定这是否是一个错误;我是否缺乏对这个操作员应该如何工作的理解;或者有一个我不明白的 optimization 正在完成。直到我仔细观察它,我才会将其关闭以生成我希望看到的代码。

在最后一个示例中,我们强制 FADDS 指令对内存操作数进行操作。 Godbolt output 相当简洁,循环本身看起来像:

.L3:
        flds    (%rdi)  # MEM[base: _51, offset: 0B]
        addq    , %rdi       #, ivtmp.6
        addq    , %rcx       #, ivtmp.8
        FADDS (%rsi)    # _31->x

        fstps   -16(%rcx)     # _28->x
        addq    , %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 取消了内部循环,只剩下外部循环。编译器生成的代码本质上与在原始问题的汇编程序模板中手工生成的代码相似。