static void Main(string[] args)
{
float f = Sum(0.1f, 0.2f);
float g = Sum(0.1f, 0.2f);
//System.Console.WriteLine("f = " + f + " and g = " + g);if (f == g)
{
System.Console.WriteLine("The sums are equal");
}
else
{
System.Console.WriteLine("The sums are Not equal");
}
Console.ReadKey();
}
static float Sum(float a, float b)
{
System.Console.WriteLine(a + b);
return a + b;
}
При запуске под отладчиком всё ок, при запуске релизной сборки под x86 без отладчика получаем "The sums are Not equal". При попытке подсмотреть раскомментировать Console.WriteLine(), код начинает вести себя по-человечески. Поведение разрешено стандартом (в процессе вычислений значения с плавающей точкой необязательно будут округляться до float), но баг всё равно красивый. Объяснение по ссылке.
Формат "R" в теории должен обеспечивать безопасное преобразование в строку и обратно. По факту, всё работает нормально только для сборок под x86, в сборке рантайма под x64 закралась ошибка, причём со времён .Net2.0. Объяснение.
Первый пример воспроизводится под .Net4 и выше, второй — начиная с .Net 2.0.
От себя:
1. Особую прелесть багам добавляет чехарда с платформами по умолчанию в разных версиях студии.
Начиная с vs 2012 в проектах по умолчанию используется платформа anycpu32bitpreferred, причём в студии она отображается так же, как и вариант по умолчанию в VS2008, Any CPU. В VS 2010 по умолчанию предлагалась платформа x86, но только для exe-сборок, что тоже не добавляет очевидности.
Если нужно работать в нескольких версиях студии, за правильно выставленнной платформой уследить сложно, я сам пару раз попадался на эти грабли. Ещё информация о изменении умолчаний в студии.
2. В тесты для всяких числодробилок однозначно стоит включать проверки и под x86, и под x64.
3. Stackoverflow крут! На оба вопроса ответы были получены за несколько часов
4. На еженедельную рассылку однозначно стоит подписаться. Подписка спрятана чёрти где, вот инструкция.
Здравствуйте, Nuseraro, Вы писали:
N>Работал в 2008 с Mono — вот там таких багов было побольше и посерьёзнее. Микрософт ещё ничего
Угу. Кстати, тут в процессе обсуждения способов избежать подобных багов нашлась подборка примеров сравнения значений с заданной погрешностью. Добавил себе в закладки.
Здравствуйте, Sinix, Вы писали: S>При запуске под отладчиком всё ок, при запуске релизной сборки под x86 без отладчика получаем "The sums are Not equal".
Ведь не первое десятилетие уже пишут — не сравнивайте значения с плавающей точкой напрямую, а только через eps.
Здравствуйте, Аноним, Вы писали:
А>Ведь не первое десятилетие уже пишут — не сравнивайте значения с плавающей точкой напрямую, а только через eps.
Не, через Single.Epsilon сравнивать не всегда корректно. Вот классическая статья на эту тему, пример оттуда
float expectedResult = 10000;
float result = +10000.000977; // The closest 4-byte float to 10,000 without being 10,000
float diff = fabs(result — expectedResult); // diff is equal to 0.000977
For numbers between 1.0 and 2.0 FLT_EPSILON represents the difference between adjacent floats. For numbers smaller than 1.0 an epsilon of FLT_EPSILON quickly becomes too large, and with small enough numbers FLT_EPSILON may be bigger than the numbers you are comparing!
Если интересуют примеры более-менее корректного сравнения на c# — см вот эту подборку.
Здравствуйте, Аноним, Вы писали:
А>Ведь не первое десятилетие уже пишут — не сравнивайте значения с плавающей точкой напрямую, а только через eps.
Тьфу, я выше глупость сморозил
Речь ведь шла о machine epsilon, не о Single.Epsilon, так?
Здравствуйте, Sinix, Вы писали:
S>float diff = fabs(result — expectedResult); // diff is equal to 0.000977 S> Single.Epsilon у нас ~ 1.401298E-45. Упс
S>Если интересуют подробности — см или хардкорную классику, или упрощённый вариант. Вот хорошее объяснение границ применимости eps оттуда:
И по обеим ссылкам нет ни слова про nextafter() и nexttoward() из IEEE754, через которые "эпсилон" для данного конкретного числа находится тривиально и гарантированно.
Впрочем, для дотнета, если верить, их не реализовали.
Здравствуйте, Аноним, Вы писали:
S>>При запуске под отладчиком всё ок, при запуске релизной сборки под x86 без отладчика получаем "The sums are Not equal".
А>Ведь не первое десятилетие уже пишут — не сравнивайте значения с плавающей точкой напрямую, а только через eps.
На самом деле во многих случаях таки можно и даже нужно. Более того, нужно знать, когда числа надо сравнивать через eps, а когда без него, и совершенно точно надо знать что такое eps.
Здравствуйте, AndrewVK, Вы писали:
AVK>При виде такого кода в мозгу моментально должна загораться красная лампочка.
Ага. Лично я уже сразу начинаю подозревать, что в данном сравнении речь идёт о кэшировании — т.е. этот флоат используется в роли чего-то типа ETag, чтобы отслеживать валидность результата вычисления.
Ну, потому что в предыдущих N случаях, когда я видел сравнение флоатов через == и поднимал панику, оказывалось, что авторы кода — неидиоты, и побитовость — намеренная.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinix, Вы писали:
S>1. Красивейший гейзенбаг:
Запощу для архива аналог для C.
#include <stdio.h>
#include <ieeefp.h>
volatile float a, b, f1;
int main() {
fpsetprec(FP_PD);
a = 0.1f;
b = 0.2f;
f1 = a + b;
printf("%d\n", f1 == (a + b));
return 0;
}
fpsetprec это для FreeBSD, для других платформ _controlfp и тому подобное. FP_PS ставит точность на single, ответ — 1. FP_PD (double), FP_PE (extended) приводит к ответу 0. Разумеется, компиляция должна быть с использованием FPU (умолчание для 32 бит), на SSE не повторяется. От уровня отладки не зависит, благодаря volatile.
В ассемблере получается
80485c8: d9 05 b8 98 04 08 flds a
80485ce: d9 05 c0 98 04 08 flds b
80485d4: de c1 faddp %st,%st(1)
80485d6: d9 1d bc 98 04 08 fstps f1
80485dc: d9 05 bc 98 04 08 flds f1
80485e2: d9 05 b8 98 04 08 flds a
80485e8: d9 05 c0 98 04 08 flds b
80485ee: de c1 faddp %st,%st(1)
80485f0: da e9 fucompp
и за счёт сравнения на стеке FPU при более широкой точности — получается данный эффект.
В стандарте C возможность подобного определяется макросом FLT_EVAL_METHOD, но в этих тестах он был равен -1, что значит, что нет определённости на этапе компиляции. Для 64-битного режима с использованием SSE он равен 0.
В дотнете, как я понял, спросить это нет возможности.
Здравствуйте, pva, Вы писали:
pva>Здравствуйте, Sinix, Вы писали:
pva>Не вопрос недели, но вот нарвался на пренеприятнейшую "багу" (?) pva> (float)(16781433u) 16781432 float pva> (float)(16781434u) 16781434 float
pva>Может кто просветит почему 16781433 не представимо в float?
У float только 24 точных двоичных разряда.
Число 16781433 == 0x1001079 == 0b1_0000_0000_0001_0000_0111_1001 — 25 двоичных разрядов.
Соответственно, оно округляется до одного из ближайших, в вашем случае
0b1_0000_0000_0001_0000_0111_1000 == 16781432, но могло быть округлено и до
0b1_0000_0000_0001_0000_0111_1010 == 16781434.
В диапазоне от 8388608 до 16777216 шаг значений float равен 1.
В диапазоне от 16777216 до 33554432 он уже равен 2.
Переходите на double или на Decimal, если нужна бо́льшая точность.
Здравствуйте, netch80, Вы писали:
pva>>Может кто просветит почему 16781433 не представимо в float? N>У float только 24 точных двоичных разряда.
Да, я в принципе догадался что проблема в представлении. Просто был немного расстроен подобным косяком. )
N>Переходите на double или на Decimal, если нужна бо́льшая точность.
Пока запатчил просто инкрементом на следующее представимое число. Но вероятно прийдется уйти на дабл.
Здравствуйте, pva, Вы писали:
pva>Может кто просветит почему 16781433 не представимо в float?
Можно поинтересоваться почему у вас float, а не double? Оптимизация по размеру или просто так было ?
Здравствуйте, _NN_, Вы писали:
pva>>Может кто просветит почему 16781433 не представимо в float? _NN>Можно поинтересоваться почему у вас float, а не double? Оптимизация по размеру или просто так было ?
Можно. Копипастил реализацию интерполяции из открытых источников.