Как работает двоичный код
Десятичные дроби в Python хранятся в формате с плавающей точкой и представлены
типом float
. Они могут быть записаны несколькими способами:
>>> 1. 1.0 >>> .1 0.1 >>> 4.2 4.2 >>> 4.2e3 # то же, что и 4.2 * 10 ** 3 == 4.2 * 1000 4200.0 >>> 4.2e-3 # то же, что и 4.2 * 10 ** (-3) == 4.2 * 0.001 0.0042
float
(x)Конструкция float(x)
принимает строку или целое число и возвращает число
с плавающей точкой, т.е. объект типа float
. Примеры:
>>> float("1.2") 1.2 >>> float(42) 42.0 >>> float("42e3") 42000.0
Для вывода чисел с плавающей точкой, как и для вывода других объектов, может
быть использована функция print
:
pi = 3.1415 print(pi) print(f"pi = {pi}")
Также существует способ указать количество выводимых знаков после запятой:
pi = 3.1415 print(f"pi = {pi:.3f}") print(f"pi = {pi:.4f}")
При преобразовании чисел с плавающей точкой в целые дробная часть отбрасывается, округления по арифметическим правилам не выполняется:
>>> int(42.9) 42
Для решения вычислительных задач может быть полезен модуль math
из
стандартной библиотеки языка Python. Для его использования нужно написать
строку import math
. Для решения задач нам понадобятся число \(\pi\)
и функция извлечения квадратного корня. Примеры их использования:
import math print(f"pi = {math.pi}") r = math.sqrt(4) print(f"square root of 4 = {r}")
Дан диаметр окружности \(d\). Найти ее длину по формуле \(length = \pi \cdot d\).
Дано значение температуры \(t\_f\) в градусах Фаренгейта. Определить значение этой же температуры в градусах Цельсия. Температура по Цельсию \(t\_c\) и температура по Фаренгейту \(t\_f\) связаны следующим соотношением:
\[t\_c = (t\_f — 32) \cdot \frac{5}{9}\]Найти значение функции \(y = 3x^6 — 6x^2 — 7\) при заданном значении \(x\). 2\).
Дано значение температуры в градусах Цельсия. Вычислить значение этой же температуры в градусах Фаренгейта. Формулу вывести самостоятельно.
Дан объем данных в мегабайтах. Перевести его в гигабайты и килобайты. Результат вывести с точностю до двух знаков после запятой.
| ||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||
Недавно я просматривал некоторые данные, которые я только что смоделировал в нашем хранилище данных, чтобы проверить, все ли выглядит правильно, и обнаружил кое-что странное: я видел некоторые события для изменения значения, где предыдущее и новое значения были равны.
Наш расчет умен; мы сравниваем вновь вычисленное значение с тем, которое мы вычислили ранее, и сохраняем его только в том случае, если оно действительно изменилось , увеличивая при этом номер версии.
Я видел, что у некоторых статистических данных, которые были у нас на складе, был очень высокий номер версии. Это означало, что произошло много изменений, и это число было подозрительно большим . Вот пример одного из событий:
{ "версия": "933", "modified_at": "2021-11-09 12:40:04.427087 UTC", "новые_значения": { "avg_compliance_percentage": "90. 34326", }, "пред_значения": { "avg_compliance_percentage": "90.34326", } },
Изменений не было, сохранялась еще одна версия!
Быстрая перепроверка нашего кода убедила меня, что для его сохранения требуется, чтобы предыдущее и новое значения были разными. Эта проверка нигде не пропускалась! Так что же происходило?
Они не были равныЯ поспешил проверить данные в нашей производственной базе данных, возможно, это была проблема с тем, как мы отправляли данные на склад. Я искал ту же статистику и нашел , они на самом деле не идентичны . И у них также было больше десятичных знаков.
{ "current_avg_compliance_percentage": "90.34326374227855", "previous_avg_compliance_percentage": "90.34326374227852" }
Прямо передо мной была огромная 0.00000000000003
разница. (3E-14)
Так это все? Задача решена? Версии на самом деле разные, и мы просто не выводим достаточной точности в наше хранилище данных?
Ну да, но на самом деле нет. Мы отправляем данные в хранилище как число с плавающей запятой с 32-битной точностью вместо двойной (64-битной), поэтому это объясняет, что в хранилище данных меньше цифр.
Но я знал, что вычисляемое конкретное значение не могло измениться так незначительно. В этом контексте значение представляет собой процент соответствия, и наименьшее возможное изменение зависит от количества поставщиков, которые есть у клиента.
minChange = 100/(numSuppliers * numControls)
Поскольку количество активных элементов управления является известным нам значением (203 на момент написания статьи), клиенту необходимо иметь не менее 16 420 361 247 948
(16 триллионов+) поставщиков. Теперь сеть Risk Ledger быстро растет, но я почти уверен, что у нас нет клиента с 16 триллионами поставщиков (что равняется 160
поставщиков для каждого человека, когда-либо жившего на этой планете).
Глядя на эти переменные с таким количеством знаков после запятой, я начал вспоминать свои занятия по численному анализу и то, как действительные числа представляются в двоичном формате.
Ясно, что такого небольшого изменения в проценте соответствия быть не могло, поэтому скорее всего фактического изменения не было . Может ли это быть примером неточности чисел с плавающей запятой?
Копать глубжеПервая гипотеза заключалась в том, что разные представления действительных чисел в нашем коде и в нашей базе данных вызывали неточности, которые приводили к сбою проверки на равенство.
Мы рассчитываем процент как значение float64
в Go и сохраняем его как десятичное число
в PostgreSQL .
Онлайн-исследования быстро выявили некоторые тревожные темы, такие как следующая проблема GitHub: https://github.com/lib/pq/issues/648.
Но пока я быстро смотрел, как мы на самом деле делаем преобразования типов, я начал думать… Если мы предположим, что они были рассчитаны как одно и то же значение, но при сохранении или получении их они изменились, не все они изменили бы так же и не показывают разницы в базе?
Может быть, вместо этого наша логика вычислений каждый раз возвращала другое значение , даже если ничего не менялось? Как значения могут отличаться, когда мы просто вычисляем среднее значение (накопление значений и последующее деление на их количество)?
Я решил разработать тест нашего алгоритма. Тест вызовет наш метод вычисления среднего с фрагментом случайно сгенерированных значений 10 000 раз, проверяя после каждого раза, совпадают ли результаты.
Разницы не было.
Благодаря какой-то гениальности я решил перемешивать срезы перед каждым вычислением, так как обычно они не всегда извлекаются в одном и том же порядке. К моему удивлению, результаты не равны !
Различия были очень, очень малы, того же порядка, что и , что я исследовал в начале… Бинго.
ПроблемаЯ обнаружил, что это ошибка с плавающей запятой.
Поскольку действительные числа не могут быть точно представлены в фиксированном пространстве, при работе с числами с плавающей запятой результат может не быть полностью представлен с требуемой точностью. Эта неточность приводит к потере информации.
С действительными числами операция сложения ассоциативна . По сути, (a + b) + c = a + (b + c)
Но с числами с плавающей запятой, если мы начнем их суммировать в разном порядке, неточности появляются и накапливаются по-разному, давая другой конечный результат.
Если вы хотите увидеть пример на Go Playground, проверьте это: https://play.golang.org/p/ZI1nP8An5Nq
РешениеКак только проблема будет выявлена и понята, она становится тривиальным для решения. В случае ошибок с плавающей запятой существует множество способов их устранения.
Первое, что я сделал, это заставил как можно больше работать с целыми числами. В этом случае целые числа могут использоваться во время шагов суммирования, а числа с плавающей запятой не требуются до последнего шага, когда происходит деление. Таким образом, мы избегаем переноса ошибок между этапами расчета.
Второе, что я сделал, это округлил результат до определенного знака после запятой. Мы отображаем усеченное целое число в пользовательском интерфейсе, так почему же мы сохраняем 13 знаков после запятой (или больше) в нашей базе данных? Я решил, что мы можем просто округлить до четвертого знака после запятой, что более чем достаточно для наших нужд.
Однако округление чисел с плавающей запятой до определенной точности является более сложной задачей, чем я первоначально думал, как упоминалось в этом сообщении от Cockroach Labs. Для нашего конкретного случая я решил использовать библиотеку montanaflynn/stats, которая работает точно, когда желаемая точность не слишком высока. Если бы наш проект действительно требовал высокой точности, нам пришлось бы перейти к другому представлению, такому как math/big.Float.
После этих изменений мой тест проходил, и я мог отдыхать, зная, что мы перестанем хранить бессмысленные строки без изменений в нашей базе данных.
Часто задаваемые вопросыЧисло с плавающей запятой — это тип числа, используемый в компьютерном программировании для представления действительных чисел с высокой степенью точности. Это способ выражения чисел с дробным компонентом, например 3,14159, таким образом, чтобы компьютер мог их хранить и обрабатывать. Числа с плавающей запятой используются в самых разных приложениях, включая научные вычисления, финансовый анализ и рендеринг трехмерной графики.
Числа с плавающей запятой используются в самых разных приложениях, включая научные вычисления, финансовый анализ и визуализацию трехмерной графики.
Числа с плавающей запятой работают путем представления действительных чисел в виде дроби (мантиссы) и показателя степени в двоичном представлении. Мантисса представляет значащие цифры числа, а показатель степени представляет величину числа. Двоичное представление позволяет эффективно выполнять арифметические операции над действительными числами, но также может привести к ошибкам округления из-за конечной точности представления.
Под капотом
Билл Веннерс, JavaWorld |
Добро пожаловать в очередной выпуск Под капотом . Эта колонка призвана дать Java-разработчикам представление о скрытой красоте их работающих Java-программ. Колонка этого месяца продолжает обсуждение, начатое в прошлом месяце, набора инструкций байт-кода виртуальной машины Java (JVM). В этой статье рассматривается арифметика с плавающей запятой в JVM и рассматриваются байт-коды, которые выполняют арифметические операции с плавающей запятой. В последующих статьях будут обсуждаться другие члены семейства байткодов.
Поддержка операций с плавающей запятой в JVM соответствует стандарту IEEE-754 1985 для операций с плавающей запятой. Этот стандарт определяет формат 32-битных и 64-битных чисел с плавающей запятой и определяет операции над этими числами. В JVM арифметика с плавающей запятой выполняется для 32-битных чисел с плавающей запятой и 64-битных чисел типа double. Для каждого байт-кода, выполняющего арифметические действия с числами с плавающей запятой, существует соответствующий байт-код, выполняющий ту же операцию с числами типа double.
Число с плавающей запятой состоит из четырех частей: знака, мантиссы, системы счисления и показателя степени. Знак либо 1, либо -1. Мантисса, всегда положительное число, содержит значащие цифры числа с плавающей запятой. Показатель степени указывает на положительную или отрицательную степень системы счисления, на которую следует умножать мантисса и знак. Четыре компонента объединяются следующим образом, чтобы получить значение с плавающей запятой:
Числа с плавающей запятой имеют несколько представлений, потому что всегда можно умножить мантисса любого числа с плавающей запятой на некоторую степень системы счисления и изменить показатель степени, чтобы получить исходное число. Например, число -5 может быть одинаково представлено любой из следующих форм в системе счисления 10:
Знак | Мантисса | 901 83 Основание экспонента|
---|---|---|
-1 | 50 | 10 -1 |
-1 | 5 | 10 0 |
-1 | 0,5 | 10 1 |
-1 | 0,05 | 10 2 |
Для каждого числа с плавающей запятой существует одно представление, которое называется быть нормализованным. Число с плавающей запятой нормализуется, если его мантисса находится в пределах диапазона, определяемого следующим соотношением:
У нормализованного числа с плавающей запятой с основанием 10 десятичная точка находится сразу слева от первой ненулевой цифры в мантиссе. Нормализованное представление -5 с плавающей запятой равно -1 * 0,5 * 10 1 . Другими словами, мантисса нормализованного числа с плавающей запятой не имеет ненулевых цифр слева от десятичной запятой и ненулевой цифры сразу справа от десятичной запятой. Любое число с плавающей запятой, не подпадающее под эту категорию, называется 9.0113 денормализованное . Обратите внимание, что число ноль не имеет нормализованного представления, потому что у него нет отличной от нуля цифры, которую можно поставить сразу справа от десятичной точки. «Зачем нормализоваться?» обычное восклицание среди нулей.
Числа с плавающей запятой в JVM используют основание из двух. Таким образом, числа с плавающей запятой в JVM имеют следующий вид:
Мантисса числа с плавающей запятой в JVM выражается как двоичное число. У нормализованной мантиссы есть двоичная точка (эквивалент десятичной точки по основанию 2) сразу слева от старшей ненулевой цифры. Поскольку в двоичной системе счисления всего две цифры — ноль и единица, — самая значащая цифра нормализованной мантиссы всегда равна единице.
Самый значащий бит числа с плавающей запятой или двойного числа — это бит знака. Мантисса занимает 23 младших бита числа с плавающей запятой и 52 младших бита двойного числа. Показатель степени, 8 бит в вещественном числе и 11 бит в двойном, находится между знаком и мантиссом. Формат float показан ниже. Бит знака показан как «s», биты экспоненты показаны как «e», а биты мантиссы показаны как «m»:0181
Знаковый бит нуля указывает на положительное число, а знаковый бит единицы указывает на отрицательное число. Мантисса всегда интерпретируется как положительное число с основанием два. Это не число с дополнением до двух. Если бит знака равен единице, значение с плавающей запятой отрицательное, но мантисса по-прежнему интерпретируется как положительное число, которое необходимо умножить на -1.
Поле экспоненты интерпретируется одним из трех способов. Показатель всех единиц указывает, что число с плавающей запятой имеет одно из специальных значений плюс или минус бесконечность или «не число» (NaN). NaN — это результат определенных операций, таких как деление нуля на ноль. Показатель степени всех нулей указывает на денормализованное число с плавающей запятой. Любой другой показатель указывает на нормализованное число с плавающей запятой.
Мантисса содержит один дополнительный бит точности помимо тех, которые появляются в битах мантиссы. Мантисса числа с плавающей запятой, занимающая всего 23 бита, имеет точность 24 бита. Мантисса двойного числа, занимающая 52 бита, имеет точность 53 бита. Самый значащий бит мантиссы предсказуем и поэтому не включается, поскольку показатель степени числа с плавающей запятой в JVM указывает, нормализовано ли число. Если экспонента состоит из нулей, число с плавающей запятой денормализовано, и известно, что старший бит мантиссы равен нулю. В противном случае число с плавающей запятой нормализуется, и известно, что старший бит мантиссы равен единице.
JVM не генерирует исключений в результате любых операций с плавающей запятой. Специальные значения, такие как положительная и отрицательная бесконечность или NaN, возвращаются в результате подозрительных операций, таких как деление на ноль. Экспонента всех единиц указывает на особое значение с плавающей запятой. Показатель всех единиц с мантиссой, все биты которой равны нулю, указывает на бесконечность. Знак бесконечности указывается битом знака. Показатель всех единиц с любой другой мантиссой интерпретируется как «не число» (NaN). JVM всегда создает одну и ту же мантисса для NaN, состоящую из нулей, за исключением самого старшего бита мантиссы, который появляется в числе. Эти значения показаны для числа с плавающей запятой ниже:
Значение | Биты с плавающей запятой (знаковая экспонента мантисса) |
---|---|
+Infin ity | 0 11111111 00000000000000000000000 |
-бесконечность | 1 11111111 0000000000000000000000000 |
NaN | 1 11111111 100000000000000000000000 |
Показатели, которые не являются ни единицами, ни нулями, указывают на степень двойки, на которую умножается нормализованная мантисса. Степень двойки можно определить, интерпретируя биты экспоненты как положительное число, а затем вычитая смещение из положительного числа. Для числа с плавающей запятой смещение равно 126. Для двойного числа смещение равно 1023. Например, поле экспоненты в вещественном числе 00000001 дает степень двойки путем вычитания смещения (126) из поля экспоненты, интерпретируемого как положительное целое число. (1). Таким образом, степень двойки равна 1 — 126, что равно -125. Это наименьшая возможная степень двойки для поплавка. С другой стороны, поле экспоненты 11111110 дает степень двойки (254 — 126) или 128. Число 128 является наибольшей степенью двойки, доступной для числа с плавающей запятой. Несколько примеров нормализованных чисел с плавающей запятой показаны в следующей таблице:
Значение | Биты с плавающей запятой (знаковая экспонента мантисса) | Несмещенная экспонента | 9 0191
---|---|---|
Наибольшее положительное (конечное) число с плавающей запятой | 0 11111110 11111111111111111111111 | 128 |
Наибольшее отрицательное (конечное) число с плавающей запятой | 1 11111110 111111111111111111111111 | 128 |
Наименьшее нормализованное число с плавающей запятой | 1 0000000 1 00000000000000000000000 | -125 |
Пи | 0 10000000 10010010000111111011011 | 2 |
Значение | Биты с плавающей запятой (знаковая экспонента мантисса) |
---|---|
Малый положительное (ненулевое) число с плавающей запятой | 0 00000000 000000000000000000000001 |
Наименьшее отрицательное (не ноль) float | 1 00000000 00000000000000000000001 |
Наибольшее денормализованное число float | 1 00000000 111111111111111111 11111 |
Положительный ноль | 0 00000000 00000000000000000000000 |
Отрицательный ноль | 1 00000000 00000000000000000000 0000 |
Плавающая среда Java раскрывает свою внутреннюю природу Приведенный ниже апплет позволяет вам поиграть с форматом с плавающей запятой.