使用FPU和C内联汇编

dem*_*moo 3 c x86 gcc inline-assembly x87

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

struct vector {
    float x1, x2, x3, x4;
};
Run Code Online (Sandbox Code Playgroud)

然后我创建了一个函数,它使用向量使用内联汇编执行一些操作:

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;
}
Run Code Online (Sandbox Code Playgroud)

一切看起来都不错,但是当我尝试使用GCC编译时,我得到了以下错误:

错误:'fadd'的操作数类型不匹配

错误:'fld'的指令后缀无效

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

Mic*_*tch 5

编码问题

我不打算提高效率(如果处理器支持它,我会使用SSE/SIMD).由于这部分任务是使用FPU堆栈,所以这里有一些问题:

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

struct vector vec[size];
Run Code Online (Sandbox Code Playgroud)

问题是你的函数返回a vector *并执行此操作:

return vec;
Run Code Online (Sandbox Code Playgroud)

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

struct vector *vec = malloc(sizeof(struct vector)*size);
Run Code Online (Sandbox Code Playgroud)

这将为阵列分配足够的空间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第三次调用该函数会失败.


为了简化代码,我建议使用a typedef来定义向量.以这种方式定义您的结构:

typedef struct {
    float x1, x2, x3, x4;
} vector;
Run Code Online (Sandbox Code Playgroud)

所有引用都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;
}
Run Code Online (Sandbox Code Playgroud)

替代方案

我不知道赋值的参数,但是如果要让你使用GCC扩展汇编程序模板来手动对带有FPU指令的向量进行操作,那么你可以用4的数组定义向量float.使用嵌套循环处理向量的每个元素,将每个元素独立地传递给汇编器模板以便一起添加.

定义vector为:

typedef struct {
    float x[4];
} vector;
Run Code Online (Sandbox Code Playgroud)

功能如下:

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;
}
Run Code Online (Sandbox Code Playgroud)

这使用了i386机器约束 tu操作数.我们允许GCC通过FPU堆栈的前两个插槽传递它们,而不是传递内存地址.tu定义为:

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

u
Second from top of 80387 floating-point stack (%st(1)). 
Run Code Online (Sandbox Code Playgroud)

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

将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;
}
Run Code Online (Sandbox Code Playgroud)

注意:%在这种情况下我删除了修饰符.理论上它应该可行,但是当针对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       #,
Run Code Online (Sandbox Code Playgroud)

在最后一个例子中,GCC展开了内环,只剩下外环.编译器生成的代码在本质上与原始问题的汇编程序模板中手动生成的代码类似.