NaN (англ. Not-a-Number ) — одно из особых состояний числа с плавающей запятой. В соответствии с IEEE 754, такое состояние задаётся через установку показателя степени в зарезервированное значение. Используется во многих математических библиотеках и математических сопроцессорах. Данное состояние может возникнуть в различных случаях, например, когда предыдущая математическая операция завершилась с неопределённым результатом, или если в ячейку памяти попало не удовлетворяющее условиям число.
К операциям, приводящим к появлению NaN в качестве ответа, относятся:
Содержание
Свойства
См. также
Примечания
Ссылки
Полезное
Смотреть что такое «NaN» в других словарях:
NaN — NaN, proviene del acrónimo en inglés Not a Number (en español: no es un número). Este acrónimo se usa generalmente en algunos lenguajes de programación para expresar un resultado imposible de calcular, como el caso de las raíces negativas,… … Wikipedia Español
Nan — or NAN may refer to one of the following. Contents 1 Acronyms 2 Places 3 People … Wikipedia
NAN — bezeichnet: eine Stadt im Norden von Thailand, siehe Nan den gleichnamigen Fluss (Maenam Nan), siehe Nan (Fluss) die gleichnamige Provinz, siehe Nan (Provinz) den Kreis Nan (chin. 南县 Nán Xiàn) der chinesischen Provinz Hunan, siehe Nan (Yiyang)… … Deutsch Wikipedia
NaN — (« Not a Number », en français « pas un nombre ») est, en informatique, une valeur ou un symbole produit par le résultat d une opération arithmétique invalide, plus spécialement dans les calculs utilisant la virgule flottante … Wikipédia en Français
Nan — … Deutsch Wikipedia
Nan — /nan/, n. 1. a word formerly used in communications to represent the letter N. 2. a female given name. * * * (as used in expressions) Hai nan Ho nan Huai nan tzu Hu nan Chi nan Nan ch ang Nan ching Nan ning Yün nan T ai nan * * * ▪ Thailand… … Universalium
Nan-ao — 南澳鄉 Staat: Republik China Koordinaten … Deutsch Wikipedia
Nan’yō — shi 南陽市 Geographische Lage in Japan … Deutsch Wikipedia
Nan’an — ist mehrdeutig. Es bezeichnet: die kreisfreie Stadt Nan an (南安市) der bezirksfreien Stadt Quanzhou in der chinesischen Provinz Fujian, siehe Nan an (Quanzhou); den Stadtbezirk Nan an (南岸区 Nan an Qū) der chinesischen regierungsunmittelbaren Stadt… … Deutsch Wikipedia
Сначала, я подумал, что это очередной вопрос из тех, которые могут задаваться на собеседовании. Наверное, если как следует пораскинуть мозгами, то можно догадаться до того, каким будет результат. Откинувшись на спинку кресла, начал размышлять, включать логику, вспоминать что-нибудь, на что можно опереться в рассуждениях. Но тщетно! Вдруг стало совершенно очевидно, что найти ответ не удается. Но почему? В чем нужно разбираться, чтобы он был найден? В математике? В языке программирования?
Ответ должен быть NaN. Но почему я не уверен в этом? Всю дорогу была уверенность в том, что любые выражения, содержащие NaN, вернут NaN. Ну разве что только если поделить NaN на ноль — в этом случае будет вызвано исключение ZeroDivisionError. Сто процентов NaN!
Ввожу выражение в ячейку блокнота:
В самом деле? Постойте:
То есть, по какой-то причине, единица в степени NaN — это единица, а вот ноль и все остальные числа в степени NaN — это NaN. Где логика? В чем дело?
Так, давайте еще раз:
Может быть я просто из-за отсутствия какой-то практической надобности в глубоких познаниях о NaN, просто о чем-то не подозревал? А может я знал, но забыл? А может еще хуже — я не знал и забыл?
Заходим на Википедию. Там данный вопрос тоже обозначен как проблема, но почему все именно так устроено, никак не объясняется. Зато узнал что:
Хотя, в то же время:
Что, согласитесь, тоже немного странно.
Ладно, с Википедии отправляемся в C99 на 182 страницу и наконец-то получаем логическое объяснение, почему pow(x, 0) возвращает 1 для любых x, даже для x равного NaN:
Если функция возводится в степень и при этом стремится к 0, то в результате получится 1, вне зависимости от того, какое значение имеет .
А если результат не зависит от числового значения функции , то 1 — является подходящим результатом, даже для NaN. Однако это по-прежнему не объясняет, почему 1 в степени NaN равна 1.
Отыскиваем еще один C99 и на 461 странице не видим никаких объяснений, просто требование того, что pow(+1, y) должно возвращать 1 для всех y, даже равных NaN. Все.
С другой стороны, объяснение, почему pow(NaN, 0)=1 является более предпочтительным, чем pow(NaN, 0)=NaN все-таки наталкивает на мысль о том, что NaN не стоит воспринимать буквально, как Not-a-Number. Допустим, в результате каких- то вычислений мы получили число, превышающее размер памяти, выделенный под данный тип чисел, например:
В результате мы получили inf, что именно это за число мы не знаем, но все же это какое-то число. Затем мы снова что-то вычислили и снова получили слишком большое число:
Разность a и b вернет NaN:
Единственная причина, по которой мы можем считать c не числом, заключается в том, что мы использовали недостаточно точные вычисления. Однако, в c под NaN все же скрывается какое-то значение. О том, что это за значение, мы не знаем. Но все же это число, а раз это число, то нет ничего удивительного в том, что pow(1, NaN)=1.
Почему же тогда pow(0, NaN)=NaN? Дело в том, что если возвести 0 в любую степень, то мы действительно получим ноль. Кроме одного единственного случая — когда степень равна 0:
Из-за чего в выражении pow(0, NaN) появляется неопределенность с конкретным значением NaN. Конечно, вероятность того, что под NaN может скрываться 0 — исчезающе мала и можно было бы принять, что pow(0, NaN)=0. Но все же лучше перестраховаться, мало ли к чему это может привести. Возможно, так и рассуждали, когда создавались стандарты.
Даже не знаю, что еще сказать… если вы заранее знали ответ, то скорее всего вам можно позавидовать, ведь сферы, где могут пригодиться такие познания, наверняка, переполнены интересными задачами. А может и наоборот. Напишите об этом в комментариях.
P.S. Поскольку NaN относится к числам с плавающей точкой, оно может быть ключом словаря:
Имеет ли смысл использовать такое на практике? Думаю, что лучше не стоит.
Продолжаем знакомство с операциями над массивами и посмотрим как они ведут себя с булевыми операциями. Предположим, имеется одномерный массив:
и мы хотим определить все числа, которые больше 5. Мы с вами уже выполняли такую операцию и для этого сначала формировали булевый массив, а затем, выделяли элементы, у индексов которых стоит значение True:
На выходе получим массив из трех элементов, которым соответствуют позиции True:
Видите, как это может быть удобно: выделить нужные элементы, не используя ни одного оператора цикла языка Python. А, значит, такая конструкция будет работать достаточно быстро (так как внутри реализована на языках Си и Fortran).
Конечно, эту запись можно еще упростить и записать в виде:
Результат будет тем же. По аналогии работают и другие булевы операторы:
Проверка на равенство
Проверка на неравенство
Проверка, что a больше b
Проверка, что a больше или равно b
array([[inf, inf], [inf, inf], [inf, inf]])
Здесь NumPy нас лишь предупредил, что встретилось деление на ноль, но расчеты были завершены и все элементы равны inf.
Что это за значение inf? Это сокращение от английского слова infinity – бесконечность. Действительно, при делении на 0 получаем бесконечность. Именно это и указано в значениях элементов массива. Благодаря использованию этого специального значения, NumPy избежал ошибки деления на 0. Причем, inf – это полноценный элемент массивов. Его можно непосредственно задать при определении:
И, далее, он может участвовать в вычислениях. Например, умножим b на ноль и посмотрим, что получится:
Последний элемент превратился в nan. Это еще одно сокращение от английского:
not a number (не число)
То есть, значение nan указывает, что в результате арифметической операции третий элемент перестал быть каким-либо числовым значением. Причем, это определение оказывается «прилипчивым». Например, сложим все элементы массива:
То есть, любые арифметические операции с nan приводят к nan.
Функции isnan и isinf
Так как элементы inf и nan не относятся к числам, то для их идентификации, проверки, что текущий элемент массива принимает одно из этих значений, существуют функции isnan() и isinf(). Они возвращают True, если элемент равен nan и inf и Flase – в противном случае. Посмотрим как можно их использовать в программе. Пусть имеется массив:
к которому применим эти две функции:
На выходе имеем массив с булевыми значениями и True стоит на местах inf (при вызове isinf) и nan (при вызове isnan). Далее, используя этот массив можно исключить нечисловые элементы из массива, например, так:
Здесь исключаются все элементы inf, а операция
indx инвертирует булевы значения. Аналогично можно отфильтровать значения nan.
Дополнительные функции: isfinite, iscomplex, isreal
Часто, при работе с массивами требуется определить: являются ли его элементы конечными числами. Для этого используется еще одна функция – isfinit():
Соответственно, все не числовые элементы помечены как False, а числовые – как True.
Далее, мы можем уточнять тип числа: комплексное или действительное, с помощью функций iscompex() и isreal(). Например:
Обратите внимание, несмотря на то, что тип данных у всех элементов массива complex128 (посмотреть можно через a.dtype), последний элемент функция iscomplex() пометила как False, так как мнимая часть равна нулю.
Аналогично работает функция isreal():
Только теперь True помечены действительные числа, а False – все остальные. Но, применяя эту функцию к массиву b:
получим все значения True. То есть, специальные значения nan и inf отмечаются как действительные.
Функции logical_and, logical_or, logical_not и logical_xor
В NumPy можно выполнять стандартные булевы операции И, ИЛИ, НЕ, исключающее ИЛИ, применительно к данным массивов. Например, зададим два массива так, чтобы попарно элементы образовывали все возможные комбинации:
И, затем, применим к ним логические операции:
Получили вполне ожидаемые результаты в соответствии с таблицами истинности этих операций.
Все те же операции можно проводить и с числовыми значениями, полагая, что 0 – это False, а любое другое число – True. Например, два таких массива:
Будут вести себя идентично массивам X, Y при булевых операциях:
Видео по теме
#2. Основные типы данных. Создание массивов функцией array() | NumPy уроки
#3. Функции автозаполнения, создания матриц и числовых диапазонов | NumPy уроки
#4. Свойства и представления массивов, создание их копий | NumPy уроки
#5. Изменение формы массивов, добавление и удаление осей | NumPy уроки
#6. Объединение и разделение массивов | NumPy уроки
Уровень 30. Ответы на вопросы к собеседованию по теме уровня
Что такое NaN?
NaN (англ. Not-a-Number) — одно из особых состояний числа с плавающей запятой. Используется во многих математических библиотеках и математических сопроцессорах. Данное состояние может возникнуть в различных случаях, например, когда предыдущая математическая операция завершилась с неопределённым результатом, или если в ячейку памяти попало не удовлетворяющее условиям число.
К операциям, приводящим к появлению NaN в качестве ответа, относятся:
В некоторых языках программирования есть «тихий» и «сигнальный» NaN: первый, попав в любую операцию, возвращает NaN, второй — вызывает аварийную ситуацию. Обычно «тихий» или «сигнальный» определяется старшим битом мантиссы.
NaN не равен ни одному другому значению (даже самому себе[2]); соответственно, самый простой метод проверки результата на NaN — это сравнение полученной величины с самой собой.
Поведение других операций сравнения зависит от языка. Одни языки дают ложь[3] (так что a a по-разному ведут себя с NaN), другие — выбрасывают аварию даже для «тихого» NaN.
Любая нетривиальная операция, принимающая «тихий» NaN как аргумент, всегда возвращает NaN вне зависимости от значения других аргументов. Единственными исключениями из этого правила являются функции max и min, которые возвращают значение «второго» аргумента (отличного от NaN). Тривиальные операции, являющиеся тождеством, обрабатываются особо: так, например, 1NaN равно 1.
Как получить бесконечность в Java?
В Java тип double имеет специальные значения для понятий «плюс бесконечность» и «минус бесконечность». Положительное число, разделенное на 0.0, дает «плюс бесконечность», а отрицательное – «минус бесконечность». Этим понятиям соответствуют специальные константы типа Double :
Любая операция, где есть NaN, дает в результате NaN.
Как проверить, что в результате вычисления получилась бесконечность?
Все сводится к выводу System.out.println()
Что такое битовая маска?
Битовая маска — это когда хранится много различных логических значений (true/false) в виде одного целого числа. При этом каждому boolean-значению соответствует определенный бит.
Где применяют битовые маски?
В основном там, где надо компактно хранить много информации об объектах. Когда хранишь много информации об объекте, всегда наберется пара десятков логических переменных. Вот их всех удобно хранить в одном числе. Именно хранить. Т.к. пользоваться им в работе не так уж удобно.
Как установить бит в единицу в битовой маске?
Опираясь на лекции можно ответить таким кодом:
Вывод такой:
Как установить бит в ноль в битовой маске?
Вывод:
Я взял число 15, так как на нем более наглядно видно, куда устанавливается 0.
Как получить значение определенного бита в битовой маске?
Вывод:
C 0 все понятно, на том месте и вправду 0. А переменная d возвращает значение запрашиваемого бита (в 10-ой системе).
Что такое ленивое вычисление выражения?
Это ленивые вычисления (lazy evaluation). В ленивых вычислениях ни один параметр не вычисляется, пока в нем нет необходимости. Программы фактически начинаются с конца и работают от конца к началу. Программа вычисляет, что должно быть возвращено, и продолжает движение назад, чтобы определить, какое значение для этого требуется. В сущности каждая функция вызывается с promise’ами для каждого параметра. Когда для вычисления необходимо значение, тогда выполняется promise. Поскольку код выполняется только тогда, когда необходимо значение, это называется вызов по необходимости (call-by-need). В традиционных языках программирования вместо promise’ов передаются значения, это называется вызов по значению(call-by-value).
Технология программирования «вызов по необходимости» имеет ряд преимуществ. Потоки имплементируются автоматически. Ненужные значения никогда не вычисляются. Однако, поведение ленивых программ часто трудно предсказать. В программах типа «вызов по значению» порядок вычисления довольно предсказуем, поэтому любые time- или sequence-based вычисления относительно легко имплемнтировать. В ленивых языках, где специальные конструкции, например, monads, необходимы для описания явно упорядоченных событий, это намного труднее. Все это также делает связь с другими языками более трудной.
Существуют языки программирования, например, Haskell и Clean, использующие ленивое программирование по умолчанию. Кроме того, для некоторых языков, таких как Scheme, ML и другие, существуют ленивые версии.
Иногда, откладывая вычисления до тех пор, пока не понадобится их значение, вы можете оптимизировать скорость выполнения программы или реструктурировать программу в более понятную форму. Несмотря на свою ценность, методы ленивого программирования не слишком широко используются или даже не очень известны. Подумайте о том, чтобы добавить их в ваш арсенал.
&& — это логическое «и». (В этом случае имеют место ленивые вычисления: некоторые вычисления опускаются, когда результат и так ясен)
& — это побитовое «и» (Если применить этот оператор к переменным типа Boolean, то ленивых вычислений происходить не будет)
Что нужно знать про арифметику с плавающей запятой
В далекие времена, для IT-индустрии это 70-е годы прошлого века, ученые-математики (так раньше назывались программисты) сражались как Дон-Кихоты в неравном бою с компьютерами, которые тогда были размером с маленькие ветряные мельницы. Задачи ставились серьезные: поиск вражеских подлодок в океане по снимкам с орбиты, расчет баллистики ракет дальнего действия, и прочее. Для их решения компьютер должен оперировать действительными числами, которых, как известно, континуум, тогда как память конечна. Поэтому приходится отображать этот континуум на конечное множество нулей и единиц. В поисках компромисса между скоростью, размером и точностью представления ученые предложили числа с плавающей запятой (или плавающей точкой, если по-буржуйски).
Арифметика с плавающей запятой почему-то считается экзотической областью компьютерных наук, учитывая, что соответствующие типы данных присутствуют в каждом языке программирования. Я сам, если честно, никогда не придавал особого значения компьютерной арифметике, пока решая одну и ту же задачу на CPU и GPU получил разный результат. Оказалось, что в потайных углах этой области скрываются очень любопытные и странные явления: некоммутативность и неассоциативность арифметических операций, ноль со знаком, разность неравных чисел дает ноль, и прочее. Корни этого айсберга уходят глубоко в математику, а я под катом постараюсь обрисовать лишь то, что лежит на поверхности.
1. Основы
Множество целых чисел бесконечно, но мы всегда можем подобрать такое число бит, чтобы представить любое целое число, возникающее при решении конкретной задачи. Множество действительных чисел не только бесконечно, но еще и непрерывно, поэтому, сколько бы мы не взяли бит, мы неизбежно столкнемся с числами, которые не имеют точного представления. Числа с плавающей запятой — один из возможных способов предсталения действительных чисел, который является компромиссом между точностью и диапазоном принимаемых значений.
Число с плавающей запятой состоит из набора отдельных разрядов, условно разделенных на знак, экспоненту порядок и мантиссу. Порядок и мантисса — целые числа, которые вместе со знаком дают представление числа с плавающей запятой в следующем виде:
Математически это записывается так:
Основание определяет систему счисления разрядов. Математически доказано, что числа с плавающей запятой с базой B=2 (двоичное представление) наиболее устойчивы к ошибкам округления, поэтому на практике встречаются только базы 2 и, реже, 10. Для дальнейшего изложения будем всегда полагать B=2, и формула числа с плавающей запятой будет иметь вид:
Что такое мантисса и порядок? Мантисса – это целое число фиксированной длины, которое представляет старшие разряды действительного числа. Допустим наша мантисса состоит из трех бит (|M|=3). Возьмем, например, число «5», которое в двоичной системе будет равно 1012. Старший бит соответствует 2 2 =4, средний (который у нас равен нулю) 2 1 =2, а младший 2 0 =1. Порядок – это степень базы (двойки) старшего разряда. В нашем случае E=2. Такие числа удобно записывать в так называемом «научном» стандартном виде, например «1.01e+2». Сразу видно, что мантисса состоит из трех знаков, а порядок равен двум.
Допустим мы хотим получить дробное число, используя те же 3 бита мантиссы. Мы можем это сделать, если возьмем, скажем, E=1. Тогда наше число будет равно
Обратите внимание, что одно и то же число имеет несколько представлений. Это не удобно для оборудования, т.к. нужно учитывать множественность представлния при сравнении чисел и при выполнении над ними арифметических операций. Кроме того, это не экономично, поскольку число представлений — конечное, а повторения уменьшают множество чисел, которые вообще могут быть представлены. Поэтому уже в самых первых машинах начали использовать трюк, делая первый бит мантиссы всегда положительным. Такое предаставление назвали нормализованным.
Это экономит один бит, так как неявную единицу не нужно хранить в памяти, и обеспечивает уникальность представления числа. В нашем примере «2» имеет единственное нормализованное представление («1.000e+1»), а мантисса хранится в памяти как «000», т.к. старшая единица подразумевается неявно. Но в нормализованном представлении чисел возникает новая проблема — в такой форме невозможно представить ноль.
Строго говоря, нормализованное число имеет следующий вид:
Качество решения задач во многом зависит от выбора представления чисел с плавающей запятой. Мы плавно подошли к проблеме стандартизации такого представления.
2. Немного истории
В 60-е и 70-е годы не было единого стандарта представления чисел с плавающей запятой, способов округления, арифметических операций. В результате программы были крайне не портабельны. Но еще большей проблемой было то, что у разных компьютеров были свои «странности» и их нужно было знать и учитывать в программе. Например, разница двух не равных чисел возвращала ноль. В результате выражения «X=Y» и «X-Y=0» вступали в противоречие. Умельцы обходили эту проблему очень хитрыми трюками, например, делали присваивание «X=(X-X)+X» перед операциями умножения и деления, чтобы избежать проблем.
Инициатива создать единый стандарт для представления чисел с плавающей запятой подозрительно совпала с попытками в 1976 году компанией Intel разработать «лучшую» арифметику для новых сопроцессоров к 8086 и i432. За разработку взялись ученые киты в этой области, проф. Джон Палмер и Уильям Кэхэн. Последний в своем интервью высказал мнение, что серьезность, с которой Intel разрабатывала свою арифметику, заставила другие компании объединиться и начать процесс стандартизации.
Все были настроены серьезно, ведь очень выгодно продвинуть свою архитектуру и сделать ее стандартной. Свои предложения представили компании DEC, National Superconductor, Zilog, Motorola. Производители мейнфреймов Cray и IBM наблюдали со стороны. Компания Intel, разумеется, тоже представила свою новую арифметику. Авторами предложенной спецификации стали Уильям Кэхэн, Джероми Кунен и Гарольд Стоун и их предложение сразу прозвали «K-C-S».
Практически сразу же были отброшены все предложения, кроме двух: VAX от DEC и «K-C-S» от Intel. Спецификация VAX была значительно проще, уже была реализована в компьютерах PDP-11, и было понятно, как на ней получить максимальную производительность. С другой стороны в «K-C-S» содержалось много полезной функциональности, такой как «специальные» и «денормализованные» числа (подробности ниже).
В «K-C-S» все арифметические алгоритмы заданы строго и требуется, чтобы в реализации результат с ними совпадал. Это позволяет выводить строгие выкладки в рамках этой спецификации. Если раньше математик решал задачу численными методами и доказывал свойства решения, не было никакой гарантии, что эти свойства сохранятся в программе. Строгость арифметики «K-C-S» сделала возможным доказательство теорем, опираясь на арифметику с плавающей запятой.
Компания DEC сделала все, чтобы ее спецификацию сделали стандартом. Она даже заручилась поддержкой некоторых авторитетных ученых в том, что арифметика «K-C-S» в принципе не может достигнуть такой же производительности, как у DEC. Ирония в том, что Intel знала, как сделать свою спецификацию такой же производительной, но эти хитрости были коммерческой тайной. Если бы Intel не уступила и не открыла часть секретов, она бы не смогла сдержать натиск DEC.
Подробнее о баталиях при стандартизации смотрите в интервью профессора Кэхэна, а мы рассмотрим, как выглядит представление чисел с плавающей запятой сейчас.
3. Представление чисел с плавающей запятой сегодня
Разработчики «K-C-S» победили и теперь их детище воплотилось в стандарт IEEE754. Числа с плавающей запятой в нем представлены в виде знака (s), мантиссы (M) и порядка (E) следующим образом:
Замечание. В новом стандарте IEE754-2008 кроме чисел с основанием 2 присутствуют числа с основанием 10, так называемые десятичные (decimal) числа с плавающей запятой.
Чтобы не загромождать читателя чрезмерной информацией, которую можно найти в Википедии, рассмотрим только один тип данных, с одинарной точностью (float). Числа с половинной, двойной и расширенной точностью обладают теми же особенностями, но имеют другой диапазон порядка и мантиссы. В числах одинарной точности (float/single) порядок состоит из 8 бит, а мантисса – из 23. Эффективный порядок определяется как E-127. Например, число 0,15625 будет записано в памяти как
Рисунок взят из Википедии
3.1 Специальные числа: ноль, бесконечность и неопределенность
Неопределенность или NaN (от not a number) – это представление, придуманное для того, чтобы арифметическая операция могла всегда вернуть какое-то не бессмысленное значение. В IEEE754 NaN представлен как число, в котором E=Emax+1, а мантисса не нулевая. Любая операция с NaN возвращает NaN. При желании в мантиссу можно записывать информацию, которую программа сможет интерпретировать. Стандартом это не оговорено и мантисса чаще всего игнорируется.
Вернемся к примеру. Наш Emin=-1. Введем новое значение порядка, E=-2, при котором числа являются денормализованными. В результате получаем новое представление чисел:
Интервал от 0 до 0,5 заполняют денормализованные числа, что дает возможность не проваливаться в 0 рассмотренных выше примерах (0,5-0,25 и 1,5-1,25). Это сделало представление более устойчиво к ошибкам округления для чисел, близких к нулю.
Но роскошь использования денормализованного представления чисел в процессоре не дается бесплатно. Из-за того, что такие числа нужно обрабатывать по-другому во всех арифметических операциях, трудно сделать работу в такой арифметике эффективной. Это накладывает дополнительные сложности при реализации АЛУ в процессоре. И хоть денормализованные числа очень полезны, они не являются панацеей и за округлением до нуля все равно нужно следить. Поэтому эта функциональность стала камнем преткновения при разработке стандарта и встретила самое сильное сопротивление.
3.4 Очередность чисел в IEEE754
Одна из удивительных особенностей представления чисел в формате IEEE754 состоит в том, что порядок и мантисса расположены друг за другом таким образом, что вместе образуют последовательность целых чисел для которых выполняется:
4.2 Неассоциативность арифметических операций
В арифметике с плавающей запятой правило (a*b)*c = a*(b*c) не выполняется для любых арифметических операций. Например,
Допустим у нас есть программа суммирования чисел.
Некоторые компиляторы по умолчанию могут переписать код для использования нескольких АЛУ одновременно (будем считать, что n делится на 2):
Так как операции суммирования не ассоциативны, эти две программы могут выдать различный результат.
4.3 Числовые константы
Помните, что не все десятичные числа имеют двоичное представление с плавающей запятой. Например, число «0,2» будет представлено как «0,200000003» в одинарной точности. Соответственно, «0,2 + 0,2 ≈ 0,4». Абсолютная погрешность в отдельном случае может и не высока, но если использовать такую константу в цикле, можем получить накопленную погрешность.
4.4 Выбор минимума из двух значений
4.5 Сравнение чисел
Очень распространенная ошибка при работе с float-ами возникает при проверке на равенство. Например,
Ошибка здесь, во-первых, в том, что 0,2 не имеет точного двоичного представления, а во-вторых 0,2 – это константа двойной точности, а переменная fValue – одинарной, и никакой гарантии о поведении этого сравнения нет.
Лучший, но все равно ошибочный способ, это сравнивать разницу с допустимой абсолютной погрешностью:
Недостаток такого подхода в том, что погрешность представления числа увеличивается с ростом самого этого числа. Так, если программа ожидает «10000», то приведенное равенство не будет выполняться для ближайшего соседнего числа (10000,000977). Это особенно актуально, если в программе имеется преобразование из одинарной точности в двойную.
Выбрать правильную процедуру сравнения сложно и заинтересованных читателей я отсылаю к статье Брюса Доусона. В ней предлагается сравнивать числа с плавающей запятой преобразованием к целочисленной переменной. Это — лучший, хотя и не портабельный способ:
5. Проверка полноты поддержки IEE754
Думаете, что если процессоры полностью соответствуют стандарту IEEE754, то любая программа, использующая стандартные типы данных (такие как float/double в Си), будет выдавать один и тот же результат на разных компьютерах? Ошибаетесь. На портабельность и соответствие стандарту влияет компилятор и опции оптимизации. Уильям Кэхэн написал программу на Си (есть версия и для Фортрана), которая позволяет проверить удовлетворяет ли связка «архитектура+компилятор+опции» IEEE754. Называется она «Floating point paranoia» и ее исходные тексты доступны для скачивания. Аналогичная программа доступна для GPU. Так, например, компилятор Intel (icc) по умолчанию использует «расслабленную» модель IEEE754, и в результате не все тесты выполняются. Опция «-fp-model precise» позволяет компилировать программу с точным соответствием стандарту. В компиляторе GCC есть опция «-ffast-math», использование которой приводит к несоответствию IEEE754.
Заключение
Напоследок поучительная история. Когда я работал над тестовым проектом на GPU, у меня была последовательная и параллельная версия одной программы. Сравнив время выполнения, я был очень обрадован, так как получил ускорение в 300 раз. Но позже оказалось, что вычисления на GPU «разваливались» и обращались в NaN, а работа с ними в GPU была быстрее, чем с обычными числами. Интересно было другое — одна и та же программа на эмуляторе GPU (на CPU) выдавала корректный результат, а на самом GPU – нет. Позже оказалось, что проблема была в том, что этот GPU не поддерживал полностью стандарт IEEE754 и прямой подход не сработал.
Сейчас арифметика с плавающей запятой почти совершенна. Практически всегда наивный подход сработает, и программа, не учитывающая все ее особенности, выдаст правильный результат, а описанные подводные камни касаются только экзотических случаев. Но нужно всегда оставаться бдительным: в таком вопросе как компьютерная математика легко наступить на грабли.