Using FPU with C inline assembly

a 夏天 提交于 2019-12-05 23:11:18

Coding Issues

I'm not looking at making this efficient (I'd be using SSE/SIMD if the processor supports it). Since this part of the assignment is to use the FPU stack then here are some concerns I have:

Your function declares a local stack based variable:

struct vector vec[size];

The problem is that your function returns a vector * and you do this:

return vec;

This is very bad. The stack based variable could get clobbered after the function returns and before the data gets consumed by the caller. One alternative is to allocate memory on the heap rather than the stack. You can replace struct vector vec[size]; with:

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

This would allocate enough space for an array of size number of vector. The person who calls your function would have to use free to deallocate the memory from the heap when finished.


Your vector structure uses float, not double. The instructions FLDL, FADDL, FSTL all operate on double (64-bit floats). Each of these instructions will load and store 64-bits when used with a memory operand. This would lead to the wrong values being loaded/stored to/from the FPU stack. You should be using FLDS, FADDS, FSTS to operate on 32-bit floats.


In the assembler templates you use the g constraint on the inputs. This means the compiler is free to use any general purpose registers, a memory operand, or an immediate value. FLDS, FADDS, FSTS do not take immediate values or general purpose registers (non-FPU registers) so if the compiler attempts to do so it will likely produce errors similar to Error: Operand type mismatch for xxxx.

Since these instructions understand a memory reference use m instead of g constraint. You will need to remove the & (ampersands) from the input operands since m implies that it will be dealing with the memory address of a variable/C expression.


You don't pop the values off the FPU stack when finished. FST with a single operand copies the value at the top of the stack to the destination. The value on the stack remains. You should store it and pop it off with an FSTP instruction. You want the FPU stack to be empty when your assembler template ends. The FPU stack is very limited with only 8 slots available. If the FPU stack is not clear when the template completes then you run the risk of the FPU stack overflowing on subsequent calls. Since you leave 4 values on the stack on each call, calling the function adding a third time should fail.


To simplify the code a bit I'd recommend using a typedef to define vector. Define your structure this way:

typedef struct {
    float x1, x2, x3, x4;
} vector;

All references to struct vector can simply become vector.


With all of these things in mind your code could look something like this:

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

Alternative Solutions

I don't know the parameters of the assignment, but if it were to make you use GCC extended assembler templates to manually do an operation on the vector with an FPU instruction then you could define the vector with an array of 4 float. Use a nested loop to process each element of the vector independently passing each through to the assembler template to be added together.

Define the vector as:

typedef struct {
    float x[4];
} vector;

The function as:

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

This uses the i386 machine constraints t and u on the operands. Rather than passing a memory address we allow GCC to pass them via the top two slots on the FPU stack. t and u are defined as:

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

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

The no operand form of FADDP does this:

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

We pass the two values to add at the top of the stack and perform an operation leaving ONLY the result in ST(0). We can then get the assembler template to copy the value on the top of the stack and pop it off automatically for us.

We can use an output operand of =t to specify the value we want moved is from the top of the FPU stack. =t will also pop (if needed) the value off the top of FPU stack for us. We can also use the top of the stack as an input value too! If the output operand is %0 we can reference it as an input operand with the constraint 0 (which means use the same constraint as operand 0). The second vector value will use the u constraint so it is passed as the second FPU stack element (ST(1))

A slight improvement that could potentially allow GCC to optimize the code it generates would be to use the % modifier on the first input operand. The % modifier is documented as:

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 ‘%’.

Because x+y and y+x yield the same result we can tell the compiler that it can swap the operand marked with % with the one defined immediately after in the template. "0"(v1[i].x[e]) could be changed to "%0"(v1[i].x[e])

Disadvantages: We've reduced the code in the assembler template to a single instruction, and we've used the template to do most of the work setting things up and tearing it down. The problem is that if the vectors are likely going to be memory bound then we transfer between FPU registers and memory and back more times than we may like it to. The code generated may not be very efficient as we can see in this Godbolt output.


We can force memory usage by applying the idea in your original code to the template. This code may yield more reasonable results:

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

Note: I've removed the % modifier in this case. In theory it should work, but GCC seems to emit less efficient code (CLANG seems okay) when targeting x86-64. I'm unsure if it is a bug; whether my understanding is lacking in how this operator should work; or there is an optimization being done I don't understand. Until I look at it closer I am leaving it off to generate the code I would expect to see.

In the last example we are forcing the FADDS instruction to operate on a memory operand. The Godbolt output is considerably cleaner, with the loop itself looking like:

.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       #,

In this final example GCC unwound the inner loop and only the outer loop remains. The code generated by the compiler is similar in nature to what was produced by hand in the original question's assembler template.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!