Проблемы с кодированием
Я не стремлюсь сделать это эффективным (я бы использовал 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
g
— очень плохая идея. Посмотрите на сгенерированный ассемблерный код, чтобы увидеть, что недопустимо, но я предполагаю, что это будет из-за них. - person Jester   schedule 29.05.2016