Почему отличается точность с плавающей точкой по C# при разделении круглыми скобками и при разделении операторами?

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

5
задан angularsen 22 March 2010 в 09:58
поделиться

2 ответа

Я не смог найти ссылку, подтверждающую это, но я думаю, что это обусловлено следующим:

  • операции с плавающей запятой вычисляются с точностью, доступной в оборудовании, это означает, что они могут выполняться с большей точностью, чем float .
  • присвоение промежуточной переменной result2 вызывает округление до точности с плавающей запятой, но единственное выражение для rsult1 вычисляется полностью с собственной точностью перед округлением в меньшую сторону.

Кстати, тестирование float или double с == всегда опасно. Модульное тестирование Microsoft предоставляет am Assert.AreEqual (ожидаемое значение с плавающей запятой, фактическое значение с плавающей точкой, дельта с плавающей запятой) , где вы можете решить эту проблему с помощью подходящей дельты.

7
ответ дан 18 December 2019 в 10:43
поделиться

Хенк совершенно прав. Просто чтобы добавить к этому немного.

Что здесь происходит, так это то, что если компилятор генерирует код, который сохраняет операции с плавающей запятой «на кристалле», то они могут выполняться с более высокой точностью. Если компилятор генерирует код, который периодически перемещает результаты обратно в стек, то каждый раз, когда они это делают, дополнительная точность теряется.

Выбирает ли компилятор генерировать код с более высокой точностью или нет, зависит от всех видов неуказанных деталей: компилировали ли вы отладку или продали, работаете ли вы в отладчике или нет, являются ли числа с плавающей запятой в переменных или константах, какую архитектуру микросхемы имеет конкретная машина и т. д.

По сути, вам гарантируется 32-битная точность ИЛИ ЛУЧШЕ, но вы НИКОГДА не можете предсказать, получите ли вы точность лучше 32-битной или нет. Поэтому вы НЕ обязаны полагаться на 32-битную точность, потому что это не гарантия, которую мы вам даем.Иногда мы добиваемся большего, а иногда нет, и если иногда вы получаете лучшие результаты бесплатно, не жалуйтесь на это.

Хенк сказал, что не может найти упоминания об этом. Это раздел 4.1.6 спецификации C #, в котором говорится:

Операции с плавающей запятой могут выполняться с более высокой точностью, чем тип результата операции. Например, некоторые аппаратные архитектуры поддерживают "расширенный" или "длинный двойной" тип с плавающей запятой с большим диапазоном и точностью, чем тип double, и неявно выполнять все операции с плавающей запятой, используя этот тип более высокой точности. Только при чрезмерных затратах на производительность такие аппаратные архитектуры могут быть заставлены выполнять операции с плавающей запятой с меньшей точностью, а не требует реализации, чтобы потерять как производительность, так и точность, C # позволяет использовать тип с более высокой точностью для всех операций с плавающей запятой . Помимо получения более точных результатов, это редко дает измеримый эффект.

Что вам следует делать: во-первых, всегда используйте двойные. Нет никаких причин использовать числа с плавающей запятой в арифметике. Используйте числа с плавающей запятой для хранилища , если хотите; если у вас их миллион и вы хотите использовать четыре миллиона байтов вместо восьми миллионов байтов, это разумное использование для чисел с плавающей запятой. Но это СТОИТ во время выполнения, потому что чип оптимизирован для выполнения 64-битной математики, а не 32-битной.

Во-вторых, не полагайтесь на точность или воспроизводимость результатов с плавающей запятой. Небольшие изменения условий могут вызвать небольшие изменения в результатах.

9
ответ дан 18 December 2019 в 10:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: