All streams
Search
Write a publication
Pull to refresh

Comments 58

  • «Плавающая запятая является случайной/ненадежной»: арифметика с плавающей запятой является детерминированной и следует строгим правилам IEEE754. Результаты могут быть неожиданными, но не произвольными.

    Стандарт IEEE требует, что все реализации должны давать точные, бит-в-бит результаты для КАЖДОЙ операции, для которой результат может быть представлен, и ближайшее значение для остальных.

В теории да, но в практических реализациях это не всегда так.

Ну если вы включили флаг --ffast-math, или каким-либо другим образом ушли от стандарта, то да. Я же говорю про стандарт, которому следует по умолчанию большинство как софта, так и железа.

Вот это:

арифметика с плавающей запятой является детерминированной и следует строгим правилам IEEE754

– неверно.

Даже если не касаться того, что есть процессоры, реализующие плавающую арифметику не в соответствии с IEEE754 (например IBM z), но и все остальные следуют IEEE754 только при определённых условиях, и то не факт, поскольку никто не проверял все возможные значения. Поэтому, на мой взгляд, вы тут очень упрощаете реальную ситуацию. Я считал бы предположение о том, что любая конкретная программа на конкретном процессоре работает в точном соответствии с IEEE754 и имеет побитово предсказуемый результат, излишне оптимистичным.

Более того, приходилось сталкиваться с этой проблемой в продакшене.

Ну проверять все возможные значения ни к чему, достаточно корректно реализовать алгоритмы вычислений. Да, есть некоторое количество железа, которое отходит от ieee754. И более того, существуют вообще другие системы плавающей точки (например, позиты), но это не вносит в жизнь ни недетерминированности, ни магии.

Магии никакой нет, но недерминированность есть в том смысле, что программист не знает, запустит ли пользователь, например, его программу на процессоре Intel или AMD, а реализация плавающей арифметики на них различается.

достаточно корректно реализовать алгоритмы вычислений

А что такое, по-вашему, корректная реализация? Никто в процессоре бесконечные ряды Тейлора не суммирует, там в любом случае используются аппроксимации, причём иногда весьма неочевидные.

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

Из моего опыта, основной источник недетерминированности результата - это непостоянство порядка вычислений (например, при многопоточных вычислениях).

Я же напрямую в статье сказал, что не нужно надеяться на побитовое сравнение, если были нетривиальные вычисления.

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

Квадратный корень не является одной из четырёх арифметических операций...

От этого, типа, должно быть легче?

Видимо, кому как. Мне - да. Я часто и много работаю с точными вычислениями на числах, представленных в плавающей точке. Работает как на интеле, так и на амд. А также на большинстве современных графических ускорителях и т.п.

Приведу пример крайне широко используемого кода/статьи от Джонатана Шевчука:

https://www.cs.cmu.edu/~quake/robust.html

Кстати, можете привести пример в несколько строк C++, который даст разные результаты на intel/amd?

У меня под рукой нет, но я пару раз приводил уже это на хабре в комментариях, можно поискать здесь или в интернете, если интересно.

Нашёл всё-таки:

#include <stdio.h>
#include <string.h>
int main(void) {
  float in = 256;
  float out;
  unsigned int raw;
  asm ("rsqrtss %1,%0" : "=x"(out) : "x"(in));
  memcpy(&raw, &out, 4);
  printf("out = %x, float = %f\n", raw, out);
  return 0;
}

Intel можно посмотреть, например, здесь: https://onecompiler.com/cpp/43x3t4pmn

out = 3d7ff000, float = 0.062485

AMD можно посмотреть, например, здесь: https://www.codingshuttle.com/compilers/cpp/

out = 3d7ff800, float = 0.062492

Во-первых, разговор шёл об арифметических операциях, которыми квадратный корень не является. А во-вторых, операция rsqrtss, серьёзно?

Computes an approximate reciprocal of the square root

[cut]

The relative error for this approximation is:

|Relative Error| ≤ 1.5 ∗ 2^{−12}

Во-первых, разговор шёл об арифметических операциях, которыми квадратный корень не является

Когда это он шёл?

У вас написано:

арифметика с плавающей запятой является детерминированной и следует строгим правилам IEEE754

Если вы под "арифметикой с плавающей запятой" подразумеваете только собственно арифметические операции, то это надо бы как-то специально оговорить, потому что обычно этот термин включает все операции процессора над числами с плавающей запятой. Да и примеры в вашей статье ими не ограничиваются, включая возведение в степень.

И вы тут же цитируете фразу со словом арифметика :)

Конечно, корень можно притянуть за уши к арифметическим операциям, но это надо постараться.

Слово "арифметика" в инженерном смысле не означает именно арифметические операции в математике.

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

Ещё раз, я не говорю, что не существует железа, которое не соответствует стандарту ieee754, равно как и флаг --ffast-math в GCC никто не убирал (но подозреваю, что вы не найдёте нормального примера, который даст разницу на intel/amd).

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

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

Тем не менее, он отвечает на ваш вопрос о программе, которая даёт разные результаты на intel/amd.

но подозреваю, что вы не найдёте нормального примера, который даст разницу на intel/amd

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

Однако, как вы верно заметили, флаг --ffast-math никто не убирал.

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

Про подавляющее большинство случаев я с вами никоим образом не спорю. Я возразил только против излишней категоричности вашего утверждения.

Тем не менее, он отвечает на ваш вопрос о программе, которая даёт разные результаты на intel/amd.

Есть другая функция, которая по дизайну тоже не обязана давать одинаковые значения, rand() называется.

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

В теории да, но в практических реализациях это не всегда так.

Из опыта, основная проблема в том, что разработчики делают неверное обобщение и полагают, что можно обычным способом сравнивать значения переменных, имеющих тип float. То есть, дело не в сравнении конкретной записи – это сравнение определено как побитовое, и в стандарте оно строгое, детерминированное. Формально, два строгих float сравниваются точно. Но это не обобщается. Дело в том, что float – это не число, а некоторый алгоритм. Поэтому, концептуально, нужно сравнение понимать так, как если бы сравнивались алгоритмы.

Собственно, в статье примерно про это и написано:

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

Я бы только подчеркнул, что ни ассоциативность, ни дистрибутивность – не могут действовать "не точно", по определению. Поэтому-то и не нужно полагать, что эти свойства есть во float. Дистрибутивность, скажем, там не работает в совсем простых случаях – вот я недавно приводил пример: https://dxdt.ru/2025/08/31/16204/

Да и с неработающей ассоциативностью несложно привести пример
1е20 + (-1е20) + 1.

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

from fractions import Fraction
x = Fraction('1/10')
for _ in range(80):
    print(x)
    if x>1/2:
        x = 2*x-1
    else:
        x = 2*x
x = 1/10
for _ in range(80):
    print(x)
    if x>1/2:
        x = 2*x-1
    else:
        x = 2*x

Кстати, с ассоциативностью у меня в статье есть пример куда проще вашего (в том смысле, что никаких огромных чисел не требует).

Между прочим, большинство целых чисел тоже невозможно невозможно записать в вашем компьютере, даже если вы разрешите использовать int64.

А некоторые операции с вещественными числами можно выполнять точно, если использовать символьные вычисления. Правда это как правило непрактично.

Ну про некоторые операции с вещественными числами и в статье есть. Которые могут выполняться даже без символьных вычислений.

Я так когда-то подобрал два коэффициента масштабирования, 0.8 и 1.25, для плавного масштабирования графиков.

Самое главное, что все целые числа вычислимы. Не обязательно хранить все цифры числа в памяти одновременно, для того, чтобы с ним манипулировать.

Но где-то их хранить всё равно придётся. В какой-то памяти, не обязательно оперативной.

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

Ещё раз. Откуда возьмёте этот поток? Где он лежать будет, ожидая, пока Вы захотите на него посмотреть? Если в целом числе будет двоичных цифр на десять порядков больше, чем атомов во Вселенной? А таких чисел гораздо больше, чем тех, которе можно записать количеством цифр меньше, чем атомов во Вселенной.

А чем целое число (а оно конечное) хуже бесконечного числа пи, для которого мы умеем строить поток цифр?

Я выдвигаю гипотезу, что для любого наперёд заданного целого числа, даже сколь угодно большого, можно написать программу для современного компьютера, которая выдаст поток его цифр.

Скрытый текст

Тонкий нюанс: я ловко опустил момент, как именно указывать число, которое надо высчитывать.

Наконец-то нашёл людей кто пишет всё с нуля без ненужных библеотек и делают их сами. Я сам пишу сейчас нейронку-компаньёна на Python, но библеотеки не использую вплоть до лингвистического анализатора и автаматического генератора ответов хотя я новичок в этой сфере))

А почему вы выбрали знаковый int?

Не очень понял, для чего?

Для моделирования float.

По-прежнему не очень понимаю. Я не упаковываю всё в четыре байта, у меня есть раздельная структура с полями знака, экспонента и мантиссы:

https://github.com/ssloy/tinyfloat/blob/main/tinyfloat.h

А, в коде всё нормально, а в статье идёт упоминание об int32_t.

А, понял. В моём игрушечном языке только знаковые 32-битные числа, поэтому я извращался, когда рейтрейсер писал. И флоат приходится в int упаковывать, и константы писать, которые влезают в него... Видимо, поэтому я и упомянул про int32_t

    int fp32_flip_sign(int fp) {
        if (fp >= 0) {
            return (fp - 1073741824) - 1073741824;
        } else {
            return (fp + 1073741824) + 1073741824;
        }
    }

Ну, если нет битовой магии, то int можно использовать. Фактически, как индекс.

Используя float, можно напечатать точное значения любой целой степени двойки насколько позволяет ёмкость выбранного float.

А как это поможет? Кроме того, используя корректно реализованный флоат, что не совсем тривиальная задача. А у меня вывод на экран идёт ещё до реализации сложения. И нужен, в частности, для отладки реализации.

Да это просто интересный факт.

Кстати, ещё замечание. 10^30 -- довольно небольшое число. Не самый удачный пример.

Это отлично для точности, но плохо для диапазона: вы не можете представить и 10^{-30}, и 10^{30}, если не используете тысячи битов.

Динамический диапазон 10^60 -- это примерно 2^200. Нужно не больше 200 бит. Если взять 1000 бит, то динамический диапазон вырастет ещё в пять раз.

Ай, не цепляйтесь к словам. Даже для обычного float вам нужно 277 бит, чтобы вместить все значения. А для double и вовсе 2098 бит...

Ну да, если бы было написано сотни бит, а не тысячи битов, то придраться было бы гораздо сложнее :)

В любом случае эта работа -- хороший способ для саморазвития.

Кстати, удивлён, что не нашлось никаких открытых софтовых реализаций float арифметики. Как минимум должны же быть библиотеки эмуляции 8087 для многих популярных языков. Ну Борланд мог не распространять свой блоб для Паскаля и Си в исходниках, тем более там не была бы открытая лицензия. Но ведь были и другие.

Как-то видел, и даже использовал чью-то библиотеку трёхбайтного float для 8051, но это несколько не тот стандарт, конечно, который Вам нужен.

Открытые есть, тот же Беркли софтфлоат. Но оно слишком оптимизировано для моих нужд

А можно представить вещественное число, где часть бит используется для хранения числа, как если бы оно было целым, а часть бит для хранения позиции запятой? (извиняюсь, если мой вопрос наивный и я "переизобретаю велосипед")

Ну оно на самом деле так и есть, но с нюансом, что число это в двоичной системе... От туда и проблемы, о чем эта статья и повествует.
В БД для денег есть decimal/numeric тип, который как раз так и работает с десятичными, но в таком виде это все не очень производительно.

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

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

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

Сразу оговорюсь, я сам очень любитель, и могу нести чушь...
Но насколько я понимаю, мы либо пользуемся аппаратными методами, которые быстры и оптимально упакованы, но двоичные. Либо используем алгоритмы на вроде деления в столбик/
Иии, если не касаться денег, то во всех остальных случая двоичные выгоднее. Причем не только по производительность, но и по упаковке в памяти.
Потому что в деньгах важна точность относительно документов и именно в 10й системе счисления.
В физиках и прочем, не важная 10я система сама по себе, а важна точность.
Если же говорить про математику в чистом виде, то насколько я это понимаю, например PI мы не запишем в точном виде ни в какой, кроме PIичной системе счисления. Так что та же длинная математика со "столбиками" все равно выгоднее в двоичном виде.

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

И, как пишет автор, в компьютерных вычислениях вещественных чисел погрешность будет всегда. Так почему бы не ограничиться определённой точностью (ведь в большинстве случаев, как мне кажется, этого будет достаточно), в границах которой вычисления будут давать математическую точность, как в примере автора (0.1+0.2)+0.3 = 0.1+(0.2+0.3)

Как вы считаете?

Мне кажется про точность вы не совсем поняли мысль.
0.1+0.2 это не погрешшность компьютера, это погрешность перевода из систему в систему.
Физика и математика не привязаны к десятичной системе счисления, а конкретные константы могут быть как точными в обоих, так и более близкими(при меньшем размере) к двоичной...
Те условно если у нас константа 0.5, это 1 бит данных в двоичной при фиксированной точности, и целых 4 в десятичной при той же фиксированной точности. А если не говорить не про производительность не про хранение, то сравнение ИМХО вообще теряет смысл. (поскольку что 10, что 2, не дают точно записать например PI)

Если опускать вопрос производительности, зачем фиксировать запятую? Её же невозможно зафиксировать удовлетворительно для всех задач. Рациональные числа с бесконечной точностью покроют любой спектр задач. Только вот производительность всё же диктует...

Чтобы это выражение было истинным

Здесь нет необходимости, в бесконечной точности, достаточно одного знака после запятой, как мне кажется в большинстве программистских задач. 🤔

И как программист выбирает подходящий способ решения поставленной задачи, например для денежных операций, почему бы не использовать в вычислениях до определённой точности библиотеку вычислений с фиксированной запятой?

Так, собственно, никто не мешает использовать целочисленную арифметику, если она вам подходит, например, в финансах. Проблема приходит тогда, когда вам нужно работать одновременно с гигантскими числами, и с очень маленькими. В физике, например, сплошь и рядом такое.

💡 Спасибо за пояснение.

Sign up to leave a comment.

Articles