Мой ответ довольно длинный, поэтому я разделил его на три раздела. Поскольку вопрос касается математики с плавающей запятой, я делаю акцент на том, что на самом деле делает машина. Я также сделал это для двойной точности (64 бит), но этот аргумент применим в равной степени к любой арифметике с плавающей запятой.
Преамбула
IEEE 754 номер двойной бинарной с плавающей запятой (binary64) представляет собой номер формы
value = (-1) ^ s * (1.m51m50 ... m2m1m0 ) 2 * 2e-1023
blockquote>в 64 бит:
- Первый бит является битом знака :
1
, если число отрицательно,0
в противном случае.- Следующие 11 бит являются показателем , который является offset на 1023. Другими словами, после чтение битов экспоненты из числа двойной точности 1023 должно быть вычтено для получения мощности двух.
- Остальные 52 бита представляют собой значение (или мантисса). В мантиссе «подразумеваемый»
1.
всегда пропускается, поскольку самый старший бит любого двоичного значения равен1
.1 - IEEE 754 допускает концепцию с нулевым значением -
+0
и-0
обрабатываются по-разному:1 / (+0)
- положительная бесконечность;1 / (-0)
- отрицательная бесконечность. Для нулевых значений биты мантиссы и экспоненты равны нулю. Примечание: нулевые значения (+0 и -0) явно не классифицируются как denormal2.2 - Это не относится к денормальным номерам , которые имеют показатель смещения нуля (и подразумевается
0.
). Диапазон денормальных чисел двойной точности dmin ≤ | x | ≤ dmax, где dmin (наименьшее представимое ненулевое число) составляет 2-1023 - 51 (≈ 4,94 * 10-324) и dmax (наибольшее денормальное число, для которого мантисса полностью состоит из1
s) составляет 2-1023 + 1 - 2-1023 - 51 (≈ 2.225 * 10-308).Превращение числа двойной точности в двоичный
Существует множество онлайн-конвертеров для преобразования двойной точности (например, в binaryconvert.com ), но здесь приведен пример кода C # для получения представления IEEE 754 для числа двойной точности (я разделяю три части с двоеточиями (
:
):public static string BinaryRepresentation(double value) { long valueInLongType = BitConverter.DoubleToInt64Bits(value); string bits = Convert.ToString(valueInLongType, 2); string leadingZeros = new string('0', 64 - bits.Length); string binaryRepresentation = leadingZeros + bits; string sign = binaryRepresentation[0].ToString(); string exponent = binaryRepresentation.Substring(1, 11); string mantissa = binaryRepresentation.Substring(12); return string.Format("{0}:{1}:{2}", sign, exponent, mantissa); }
Достижение точки: исходный вопрос
(Перейти к нижней части для версии TL; DR)
Катон Джонстон (вопросник) спросил, почему 0.1 + 0.2! = 0.3.
Написано в двоичном (с двоеточиями, разделяющими три части), представления IEEE 754 значений:
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010 0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010
Обратите внимание, что мантисса состоит из повторяющихся цифр
0011
. ключ к тому, почему есть какие-либо ошибки в расчетах - 0,1, 0,2 и 0,3 не могут быть представлены в двоичной форме точно в конечном числе двоичных битов не более 1/9, 1/3 или 1/7 могут быть представлены точно в десятичных разрядах .Преобразование экспонентов в десятичные, удаление смещения и повторное добавление подразумеваемых
1
(в квадратных скобках), 0,1 и 0,2 :0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010 0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010
Чтобы добавить два числа, показатель должен быть одинаковым, т. е.
0.1 = 2^-3 * 0.1100110011001100110011001100110011001100110011001101(0) 0.2 = 2^-3 * 1.1001100110011001100110011001100110011001100110011010 sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111
Поскольку сумма не имеет вид 2n * 1. { bbb} мы увеличиваем показатель на единицу и сдвигаем десятичную ( двоичную ) точку, чтобы получить:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
В мантиссе сейчас 53 бит (53-й квадрат скобки в строке выше). Режим округления по умолчанию для IEEE 754 равен ' Round to Nearest ' - то есть, если число x падает между двумя значениями a и b выбрано значение, в котором наименьший значащий бит равен нулю.
a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011 x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1) b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
Обратите внимание, что a и b отличаются только последним битом;
...0011
+1
=...0100
. В этом случае значение с младшим значащим разрядом равно b , поэтому сумма равна:sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
TL; DR
Запись
0.1 + 0.2
в двоичном представлении IEEE 754 (с двоеточиями, разделяющими три части) и сравнивая его с0.3
, это (я положил отдельные биты в квадратные скобки):0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100] 0.3 => 0:01111111101:0011001100110011001100110011001100110011001100110[011]
Преобразован обратно к десятичной, эти значения:
0.1 + 0.2 => 0.300000000000000044408920985006... 0.3 => 0.299999999999999988897769753748...
Разница в точности равна 2-54, что составляет ~ 5.5511151231258 × 10-17 - незначительно (для многих приложений) по сравнению с исходными значениями.
Сравнение последних нескольких бит числа с плавающей запятой по своей сути опасно, как и любой, кто читает знаменитый «, что каждый компьютерный ученый должен знать о арифметике с плавающей точкой » (который охватывает все основные части этого ответа).
Большинство калькуляторов используют дополнительные охранные цифры , чтобы обойти эту проблему, так как
0.1 + 0.2
даст0.3
: последние несколько биты округлены.
Я думаю, вы неправильно поняли концепцию flavorDimension.
Аромат-аромат - это что-то вроде категории вкуса , и каждая комбинация аромата из каждого измерения даст вариант.
В вашем случае вы должны определить один аромат, называемый «тип», и другое измерение, называемое «организация». Он будет производить для каждого аромата в «организации» измерения все возможные «типы» (или двойную формулировку: для каждого «типа» он будет давать вариант для каждой организации).
Размеры аромата определяют декартова произведение , которое будет использоваться для создания вариантов.
EDIT: я попытаюсь проиллюстрировать код псевдограду:
Давайте определим некоторый «тип»: бронза, серебро и золото
Давайте определим некоторые организации: customerA, customerB, customerC
Все это productFlavors , но они относятся к двум различным размерам:
flavorDimensions("type_line", "organization")
productFlavors {
gold {
...
dimension = "type_line"
}
silver {
...
dimension = "type_line"
}
bronze {
...
dimension = "type_line"
}
customerA {
...
dimension = "organization"
}
customerB {
...
dimension = "organization"
}
customerC {
...
dimension = "organization"
}
}
Эта конфигурация будет производят 18 (3 * 3 * 2) варианта (если у вас есть 2 стандартных типа сборки: отладка и выпуск):
gold-customerA-debug; gold-customerA-релиз; gold-customerB-debug; gold-customerB-релиз; gold-customerC-debug; gold-customerC-релиз;
silver-customerA-debug; серебро-клиентА-релиз; silver-customerB-debug; серебро-клиентB-релиз; silver-customerC-debug; серебро-клиентC-релиз;
... (то же самое для бронзы)
Обратите внимание, что имя измерения абсолютно произвольно и не влияет на имена вариантов.
Размеры вкуса являются очень мощными, но если вы используете слишком много из них: это приводит к экспоненциальному взрыву числа вариантов (задача очистки после сборки может быть полезна для удаления бесполезного или нечувствительного варианта)