Использование 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'

На OS/X в XCode все работает корректно. Что не так с этим кодом?


person demoo    schedule 29.05.2016    source источник
comment
Использование ограничения g — очень плохая идея. Посмотрите на сгенерированный ассемблерный код, чтобы увидеть, что недопустимо, но я предполагаю, что это будет из-за них.   -  person Jester    schedule 29.05.2016
comment
Не ответ, но если это задумано как оптимизация, то, вероятно, это не так. Также у вас есть неопределенное поведение в том, как вы возвращаете результат.   -  person Flexo    schedule 29.05.2016
comment
По какой причине вы не используете SSE/SIMD? И вы не должны возвращать указатель на локальную (на основе стека) структуру. Все входные операнды, равные g, должны быть m. Вы не можете передавать регистры общего назначения (и непосредственные) в FLD, FADD и FST.   -  person Michael Petch    schedule 29.05.2016
comment
У меня есть код с SSE @MichaelPetch, сейчас обхожусь без этого и считаю разы, как быстрее (для школы) но я удалил эти строки, потому что это не проблема. Я пытался использовать m, но получил много таких ошибок: вход памяти 6 не адресуется напрямую.   -  person demoo    schedule 29.05.2016


Ответы (1)


Проблемы с кодированием

Я не стремлюсь сделать это эффективным (я бы использовал 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;

Функция как:

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 делает это :

Добавьте 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;
}

Примечание. В этом случае я удалил модификатор %. Теоретически это должно работать, но GCC, кажется, выдает менее эффективный код (CLANG кажется нормальным) при работе с x86-64. Я не уверен, что это ошибка; не хватает ли мне понимания того, как должен работать этот оператор; или выполняется оптимизация, которую я не понимаю. Пока я не рассмотрю его поближе, я оставлю его, чтобы сгенерировать код, который я ожидал увидеть.

В последнем примере мы заставляем инструкцию 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 раскрутил внутренний цикл, и остался только внешний цикл. Код, сгенерированный компилятором, по своей природе аналогичен коду, созданному вручную в исходном шаблоне ассемблера вопроса.

person Michael Petch    schedule 29.05.2016
comment
Вместо malloc я бы предложил передать вызывающей стороне указатель dst. Это позволяет, например, повторно использовать один и тот же буфер для нескольких вызовов. - person Peter Cordes; 30.05.2016
comment
@PeterCordes Я подумал об этом, и ваше предложение разумно. В данном случае я не собирался менять прототип, так как не был уверен, что они предусмотрены в задании. - person Michael Petch; 30.05.2016
comment
Я думаю, что с правильными ограничениями вы могли бы заставить gcc создавать такой же оптимальный asm, но при этом иметь только одну инструкцию в операторе asm. то есть попросите, чтобы один входной операнд находился на вершине стека FPU, а не в памяти. Затем в контексте, где оба входа одинаковы, gcc может выдать fadds %st(0) (если только суффикс размера операнда s не является синтаксической ошибкой в ​​​​этом контексте! Вам может понадобиться режим intel-syntax, чтобы gcc предоставил размер операнда для операнда ). Кроме того, я думаю, что вы поменяли местами %1 и %2 в своем окончательном утверждении asm. flds должен использовать только "m" операнд. - person Peter Cordes; 30.05.2016
comment
@PeterCordes И да, синтаксическая ошибка была на самом деле причиной, по которой я пошел по этому пути. Я не собирался мутить воду введением отдельного синтаксиса Intel, хотя подозреваю, что это сработает. - person Michael Petch; 30.05.2016
comment
@PeterCordes Я исправлен, оба входных операнда должны были быть m, поскольку добавление s к инструкции в качестве суффикса не позволяет использовать регистр FPU (синтаксическая ошибка) с синтаксисом AT&T. Я изменю ответ, чтобы отразить это. Теоретически я мог бы отказаться от суффикса и позволить компилятору по умолчанию использовать 32-разрядную версию, но чувствовал, что это может показаться непоследовательным и вызовет проблему, если они перейдут на использование двойного (сбой во время выполнения) - person Michael Petch; 30.05.2016
comment
Я предпочитаю, чтобы суффикс присутствовал для ясности. Учитывая обсуждение, которое я дал, я бы предпочел видеть S или L в конце, так как размер намного понятнее. оперируется. Отказаться от этого, позволяя компилятору молча выбирать, кажется более проблематичным, если перейти от поплавков к удвоениям. Можно ошибочно предположить, что шаблон ассемблера каким-то образом правильно определяет, являются ли операнды вещественными или двойными. - person Michael Petch; 30.05.2016
comment
Я согласен, что нет смысла говорить о -masm=intel, чтобы заставить компилятор заменить dword ptr ... для вас, и что ваш код сейчас является лучшим выбором для этого ответа. Мне также не нравится позволять ассемблеру выбирать размер операнда памяти по умолчанию для инструкций FP. Если бы я когда-либо собирался сделать это, чтобы разрешить операнд reg-or-memory в случае, когда он фактически использовался бы в обоих направлениях в существующей кодовой базе, мне пришлось бы поместить большой комментарий, объясняющий риски и причину, почему это стоило того. - person Peter Cordes; 30.05.2016
comment
typedef struct _vector может быть просто typedef struct { ... } vector;. Или, желательно, какое-то другое имя, потому что std::vector повсеместно используется в C++. Это похоже на использование printf в качестве имени функции на каком-то другом языке для чего-то, что не работает так же, как printf(3). - person Peter Cordes; 30.05.2016
comment
@PeterCordes Первоначально, когда я делал это, я использовал ограничение %0, используя потенциальное преимущество коммутативного свойства add. Сгенерированный код выглядел необычно, и я пошел по пути загрузки и причуды в шаблоне. Только когда я удалил % и перешел на 0, я увидел генерацию кода, которую ожидал. Хотя мои последние правки предполагают, что я, возможно, не знаю, что делаю, я почти уверен, что есть ошибка оптимизатора с кодом x86-64 и FPU. CLANG, похоже, не дает такого поведения. Код, сгенерированный GCC, похоже, возвращается к коду, сгенерированному в GCC 4.7-4.9.0. - person Michael Petch; 30.05.2016
comment
Можно увидеть разницу, если вы возьмете последний вариант кода в моем ответе и измените :"0"(v1[i].x[e]), "m"(v2[i].x[e]) на :"%0"(v1[i].x[e]), "m"(v2[i].x[e]) - person Michael Petch; 30.05.2016
comment
Интересное наблюдение. Очевидно, что генерация кода для встроенного ассемблера x87 с -mfpmath=sse не является приоритетом для разработчиков gcc. Использование "%0" для получения коммутативных входных данных, по-видимому, позволяет gcc слишком много думать об этом, и в конечном итоге он копирует их в / из локального в стеке, используя movss. -mfpmath=387 отлично подходит как с "%0", так и без него. Я не планирую подавать отчет об ошибке для этого, так как x87 устарел, и разработчикам gcc не нужно тратить время на встроенный ассемблер для этого, пока сгенерированный код действительно работает правильно. - person Peter Cordes; 30.05.2016
comment
@PeterCordes: О, мне пришло в голову зарегистрировать ошибку, когда я сделал наблюдение, но в ее нынешнем виде ценность в ней очень ограничена, поскольку то, что здесь делается, в большинстве случаев является второстепенным случаем, который вы, вероятно, только увидеть, как это происходит в академических кругах (по крайней мере, с кодом x86-64). В моем комментарии об этом в ответе говорилось только, что он создает неэффективный код, но сам код действительно работает, как и ожидалось, хотя и окольным путем. - person Michael Petch; 30.05.2016
comment
Да, я не думал, что есть какие-то проблемы с правильностью. Я мог бы представить себе использование встроенного ассемблера x87 по причинам размера кода, например. для fsincos вместо библиотеки с SSE insns, даже в современном коде, но, может быть, только для встраиваемой системы с ограниченной памятью. - person Peter Cordes; 30.05.2016