Я знаю, как точность с плавающей точкой работает в регулярных случаях, но я наткнулся на нечетную ситуацию в своем коде C#.
Почему result1 и result2 не являются тем же самым значением с плавающей точкой здесь?
const float A; // Arbitrary value
const float B; // Arbitrary value
float result1 = (A*B)*dt;
float result2 = (A*B);
result2 *= dt;
От этой страницы я полагал, что арифметика плавающая была левоассоциативна и что это означает, что значения оценены и вычислены в слева направо способ.
Полный исходный код включает Кватернионы XNA. Я не думаю, что необходимо, что мои константы и что VectorHelper. AddPitchRollYaw () делает. Тест передает очень хорошо, если я вычисляю углы подачи/списка/отклонения от курса дельты таким же образом, но поскольку код ниже его, не передает:
X
Expected: 0.275153548f
But was: 0.275153786f
[TestFixture]
internal class QuaternionPrecisionTest
{
[Test]
public void Test()
{
JoystickInput input;
input.Pitch = 0.312312432f;
input.Roll = 0.512312432f;
input.Yaw = 0.912312432f;
const float dt = 0.017001f;
float pitchRate = input.Pitch * PhysicsConstants.MaxPitchRate;
float rollRate = input.Roll * PhysicsConstants.MaxRollRate;
float yawRate = input.Yaw * PhysicsConstants.MaxYawRate;
Quaternion orient1 = Quaternion.Identity;
Quaternion orient2 = Quaternion.Identity;
for (int i = 0; i < 10000; i++)
{
float deltaPitch =
(input.Pitch * PhysicsConstants.MaxPitchRate) * dt;
float deltaRoll =
(input.Roll * PhysicsConstants.MaxRollRate) * dt;
float deltaYaw =
(input.Yaw * PhysicsConstants.MaxYawRate) * dt;
// Add deltas of pitch, roll and yaw to the rotation matrix
orient1 = VectorHelper.AddPitchRollYaw(
orient1, deltaPitch, deltaRoll, deltaYaw);
deltaPitch = pitchRate * dt;
deltaRoll = rollRate * dt;
deltaYaw = yawRate * dt;
orient2 = VectorHelper.AddPitchRollYaw(
orient2, deltaPitch, deltaRoll, deltaYaw);
}
Assert.AreEqual(orient1.X, orient2.X, "X");
Assert.AreEqual(orient1.Y, orient2.Y, "Y");
Assert.AreEqual(orient1.Z, orient2.Z, "Z");
Assert.AreEqual(orient1.W, orient2.W, "W");
}
}
Предоставленный, ошибка является небольшой и только представляет себя после большого количества повторений, но она вызвала меня некоторый большой headackes.
Я не смог найти ссылку, подтверждающую это, но я думаю, что это обусловлено следующим:
float
. result2
вызывает округление до точности с плавающей запятой, но единственное выражение для rsult1
вычисляется полностью с собственной точностью перед округлением в меньшую сторону. Кстати, тестирование float или double с ==
всегда опасно. Модульное тестирование Microsoft предоставляет am Assert.AreEqual (ожидаемое значение с плавающей запятой, фактическое значение с плавающей точкой, дельта с плавающей запятой)
, где вы можете решить эту проблему с помощью подходящей дельты.
Хенк совершенно прав. Просто чтобы добавить к этому немного.
Что здесь происходит, так это то, что если компилятор генерирует код, который сохраняет операции с плавающей запятой «на кристалле», то они могут выполняться с более высокой точностью. Если компилятор генерирует код, который периодически перемещает результаты обратно в стек, то каждый раз, когда они это делают, дополнительная точность теряется.
Выбирает ли компилятор генерировать код с более высокой точностью или нет, зависит от всех видов неуказанных деталей: компилировали ли вы отладку или продали, работаете ли вы в отладчике или нет, являются ли числа с плавающей запятой в переменных или константах, какую архитектуру микросхемы имеет конкретная машина и т. д.
По сути, вам гарантируется 32-битная точность ИЛИ ЛУЧШЕ, но вы НИКОГДА не можете предсказать, получите ли вы точность лучше 32-битной или нет. Поэтому вы НЕ обязаны полагаться на 32-битную точность, потому что это не гарантия, которую мы вам даем.Иногда мы добиваемся большего, а иногда нет, и если иногда вы получаете лучшие результаты бесплатно, не жалуйтесь на это.
Хенк сказал, что не может найти упоминания об этом. Это раздел 4.1.6 спецификации C #, в котором говорится:
Операции с плавающей запятой могут выполняться с более высокой точностью, чем тип результата операции. Например, некоторые аппаратные архитектуры поддерживают "расширенный" или "длинный двойной" тип с плавающей запятой с большим диапазоном и точностью, чем тип double, и неявно выполнять все операции с плавающей запятой, используя этот тип более высокой точности. Только при чрезмерных затратах на производительность такие аппаратные архитектуры могут быть заставлены выполнять операции с плавающей запятой с меньшей точностью, а не требует реализации, чтобы потерять как производительность, так и точность, C # позволяет использовать тип с более высокой точностью для всех операций с плавающей запятой . Помимо получения более точных результатов, это редко дает измеримый эффект.
Что вам следует делать: во-первых, всегда используйте двойные. Нет никаких причин использовать числа с плавающей запятой в арифметике. Используйте числа с плавающей запятой для хранилища , если хотите; если у вас их миллион и вы хотите использовать четыре миллиона байтов вместо восьми миллионов байтов, это разумное использование для чисел с плавающей запятой. Но это СТОИТ во время выполнения, потому что чип оптимизирован для выполнения 64-битной математики, а не 32-битной.
Во-вторых, не полагайтесь на точность или воспроизводимость результатов с плавающей запятой. Небольшие изменения условий могут вызвать небольшие изменения в результатах.