Почему отличается точность с плавающей точкой в ​​C #, когда они разделены парантезами и когда они разделены операторами? - PullRequest
5 голосов
/ 22 марта 2010

Я знаю, как работает точность с плавающей запятой в обычных случаях, но я наткнулся на странную ситуацию в моем коде C #.

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


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");
        }
    }

Конечно, ошибка невелика и проявляется только после большого количества итераций, но она вызвала у меня большие проблемы.

Ответы [ 2 ]

9 голосов
/ 22 марта 2010

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

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

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

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

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

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

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

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

7 голосов
/ 22 марта 2010

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

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

С другой стороны, проверка поплавка или двойного с == всегда опасна.В модульном тестировании Microsoft предусмотрено время Assert.AreEqual(float expected, float actual,float delta), где вы могли бы решить эту проблему с подходящей дельтой.

...