Я использую Embarcadero C++ Builder 12, и у меня возникла огромная проблема с моей степенной функцией в сборке, она называется vpow(base, exp)
. Он отлично работает с целыми числами, но мне нужно, чтобы он работал с двойными числами. Моя цель — обычный процессор ПК x86-64. Вот моя функция:
double vpow(double base, int exp) {
double result = 0.0;
__asm {
finit
fld base
fild exp
fyl2x
fld st(0)
frndint
fsub st(1), st(0)
fxch st(1)
f2xm1
fld1
fadd
fscale
fstp result
}
return result;
}
Что я делаю неправильно? Мне нужно, чтобы он работал для баз с плавающей запятой. Например, 2,5^3= 15625. На данный момент exp будет целым числом. Это 64-битная сборка, и ее необходимо запускать в C++ Builder IDE.
Я пытался
double vpow(double base, int exp) {
double result = 0.0;
__asm {
finit
fld base
fild exp
fyl2x
fld st(0)
frndint
fsub st(1), st(0)
fxch st(1)
f2xm1
fld1
fadd
fscale
fstp result
}
return result;
}
Мне просто нужно, чтобы это работало и для парных. Это уже работает для целых чисел!
х86-64, друг.
Показалась ли эта конкретная часть вашего кода подходящим местом для микрооптимизации или почему вы используете ассемблер для функции power-of? Если мне нужно оптимизировать, я стараюсь максимально избегать использования триггерных функций.
fld
загружает значение с плавающей запятой. fild
следует использовать для загрузки целого числа. Но на x86_64 обычно вместо FPU используются операции SSE.
@interjay Можешь объяснить лучше? Я попробовал более 15 способов, и мне выдали неверные числа: NaN, 1 или INF.
@TedLyngmo Я пишу всю математику на ассемблере для магистерского проекта. А также много практиковаться, потому что мне нужно будет знать ассемблер.
@VictorMelo О, ок.
@TedLyngmo Я написал все триггерные функции, но эту не смог исправить....
exp
— целое число, поэтому вам следует загрузить его с помощью fild
. Выполнение этой строки в отладчике легко покажет вам, какая строка дает неправильные результаты.
@interjay Я тоже пробовал. Но все равно даю мне неправильный ответ. Я думаю, что требуется полная переформулировка.
@TedLyngmo: Сброс всего FPU с помощью finit
для каждого вызова vpow
не является микрооптимизацией. Кроме того, использование этой формы встроенного ассемблера заставляет операнды сохраняться и перезагружаться при вводе и выводе, поскольку нет возможности сообщить компилятору, что результат уже находится в st0
, поэтому вам придется fstp
, чтобы компилятор перезагрузился. Итак, как говорит ОП, это имеет смысл только как обучающее упражнение для x87 и встроенного ассемблера.
Оба блока кода кажутся мне одинаковыми. Предполагается, что один из них подойдет int
? Или для целочисленных double
? Потому что ни один из них не использует fild
. Я не знаю, какой на самом деле компилятор использует Embarcadero, но clang — единственный из большой тройки (GCC/Clang/MSVC), который поддерживает блоки asm {}
для x86-64. (MSVC поддерживает этот синтаксис только для 32-разрядной версии x86). В любом случае, стоит проверить, как он компилируется в настоящий asm, например. используйте отладчик в режиме дизассемблирования, чтобы проверить размеры операндов на fld
, чтобы убедиться, что это qword
для входных данных double
, и dword
для fild
на int
.
@PeterCordes Я не комментировал, насколько плохо или хорошо написан ассемблерный код. Я предположил, что добавление какой-либо встроенной сборки в программу на C++ связано с попыткой выполнить какую-то оптимизацию.
В вашем минимально воспроизводимом примере отсутствуют какие-либо подробности того, что вы получаете вместо этого. Кроме того, finit
потенциально может что-то сломать, если это встроится в функцию, которая уже хранит что-либо в регистрах x87. (IDK, если бы компиляторы сделали это; им потребовалось бы выяснить, что встроенному ассемблеру не нужны все 8 слотов регистров x87 ( web.archive.org/web/20230611103408/http://www.ray.masmcode .com/…)).
@PeterCordes Я пытался это изменить, но все равно не работает.
@VictorMelo - Что пытались изменить, но не получилось как? Теперь, когда вы используете fild
, какие значения регистров вы видите с помощью отладчика на различных этапах? Где они перестали быть тем, что вы ожидали/хотели? Как выглядел ваш код для версии, работающей с целыми числами? Были ли в нем также использованы математические инструкции x87, или эта версия совершенно другая?
@PeterCordes Да, я изменил FLD на FILD и обновил его в своем вопросе. Это тоже не сработало. Я думаю, что цикл понадобится, но я не знаю, как это сделать.
для плавающей точки pow
не нужен цикл. Типичная реализация или журнал или выражение только с базовыми операциями (умножение, деление, сложение, вычитание) и целочисленными битовыми манипуляциями с битовыми шаблонами FP возможна с полиномиальной аппроксимацией в диапазоне мантиссы 0,5, 1,0) или [1,0 , 2), с отдельной обработкой показателя степени. См. [Эффективная реализация log2(__m256d) в AVX2 . Или в x87 должна быть просто последовательность из нескольких инструкций: Как: pow(real, real) в x86
@PeterCordes Я работаю с объектно-ориентированным программированием на C++, и оно не очень хорошо работает в AVX2. Есть другой способ?
Вы прочитали больше, чем заголовок вопросов и ответов, на которые я дал ссылку? Вы можете использовать тот же метод разделения мантиссы и экспоненты для скаляра или с SSE2; мой ответ там даже связывает некоторые вещи SSE. Кроме того, второй вопрос и ответ, на который я ссылаюсь, показывает встроенные блоки x87 __asm{}
.
Исправить это просто. Прочтите спецификацию FYL2X или воспользуйтесь отладчиком. Немного странно использовать сегодня инструкции x87 на x64 — они намного медленнее, чем AVX2 (хотя в некоторых компиляторах по-прежнему генерируются коды по умолчанию).
FYL2X вычисляет ST(1)*log2(ST(0)) — вы загрузили аргументы в стек в неправильном порядке! FINIT также удален, поскольку он совершенно бесполезен.
Если оказалось, что с целочисленными аргументами все работает нормально, то это может быть только из-за неадекватного тестирования.
#include <stdio.h>
double vpow(double base, int exp) {
double result = 0.0;
__asm {
fild exp // note order of arguments swapped here
fld base
fyl2x
fld st(0)
frndint
fsub st(1), st(0)
fxch st(1)
f2xm1
fld1
fadd
fscale
fstp result
}
return result;
}
int main()
{
printf("%f\n", vpow(2.5, 2));
for (int i=0; i<6; i++)
printf("%18.9g\n", vpow(1.1, i));
}
Это достаточный тестовый пример, чтобы убедиться, что он работает для двойных значений. Выход:
6.250000
1
1.1
1.21
1.331
1.4641
1.61051
Спасибо тебе большое, друг! Это сработало как шарм!
Поскольку языки ассемблера специфичны для процессора, на какой процессор вы ориентируетесь?