Ответ 1
Подумав об этом в течение некоторого времени и взглянув на объявление с исходным кодом, немного передумав, я думаю, что могу ответить на собственный вопрос. Мои гипотезы почти правильны, но не вся история.
Поскольку NumPy и Python обрабатывают числа совершенно по-другому, этот ответ состоит из двух частей.
Что действительно происходит в Python и NumPy с NaNs
NumPy
Это может быть немного специфично для платформы, но на большинстве платформ NumPy использует gcc
встроенный isnan
, который, в свою очередь, делает что-то быстро. Предупреждения во время выполнения исходят от более глубоких уровней, от аппаратного обеспечения в большинстве случаев. (NumPy может использовать несколько методов определения состояния NaN, таких как x!= X, который работает на платформах AMD 64, но с gcc
он меньше gcc
, который, вероятно, использует довольно короткий код для этой цели.)
Итак, в теории нет способа гарантировать, как NumPy обрабатывает NaN, но на практике на более общих платформах он будет делать, как говорит стандарт, потому что это то, что делает оборудование. Сам NumPy не заботится о типах NaN. (За исключением некоторых поддерживаемых NumPy не-hw-поддерживаемых типов данных и платформ.)
Python
Здесь история становится интересной. Если платформа поддерживает поплавки IEEE (в большинстве случаев), Python использует библиотеку C для арифметики с плавающей запятой и, следовательно, почти в большинстве случаев аппаратные инструкции. Таким образом, для NumPy не должно быть никаких различий.
За исключением... В Python обычно нет такой вещи, как 32-битный float. В плавающих объектах Python используется C double
, который представляет собой 64-разрядный формат. Как преобразовать специальные NaN между этими форматами? Чтобы увидеть, что происходит на практике, следующий небольшой код C помогает:
/* nantest.c - Test floating point nan behaviour with type casts */
#include <stdio.h>
#include <stdint.h>
static uint32_t u1 = 0x7fc00000;
static uint32_t u2 = 0x7f800001;
static uint32_t u3 = 0x7fc00001;
int main(void)
{
float f1, f2, f3;
float f1p, f2p, f3p;
double d1, d2, d3;
uint32_t u1p, u2p, u3p;
uint64_t l1, l2, l3;
// Convert uint32 -> float
f1 = *(float *)&u1; f2 = *(float *)&u2; f3 = *(float *)&u3;
// Convert float -> double (type cast, real conversion)
d1 = (double)f1; d2 = (double)f2; d3 = (double)f3;
// Convert the doubles into long ints
l1 = *(uint64_t *)&d1; l2 = *(uint64_t *)&d2; l3 = *(uint64_t *)&d3;
// Convert the doubles back to floats
f1p = (float)d1; f2p = (float)d2; f3p = (float)d3;
// Convert the floats back to uints
u1p = *(uint32_t *)&f1p; u2p = *(uint32_t *)&f2p; u3p = *(uint32_t *)&f3p;
printf("%f (%08x) -> %lf (%016llx) -> %f (%08x)\n", f1, u1, d1, l1, f1p, u1p);
printf("%f (%08x) -> %lf (%016llx) -> %f (%08x)\n", f2, u2, d2, l2, f2p, u2p);
printf("%f (%08x) -> %lf (%016llx) -> %f (%08x)\n", f3, u3, d3, l3, f3p, u3p);
return 0;
}
Отпечатки:
nan (7fc00000) -> nan (7ff8000000000000) -> nan (7fc00000)
nan (7f800001) -> nan (7ff8000020000000) -> nan (7fc00001)
nan (7fc00001) -> nan (7ff8000020000000) -> nan (7fc00001)
Посмотрев на строку 2, очевидно, что мы имеем то же явление, что и у Python. Таким образом, это преобразование в double
, которое вводит дополнительный бит is_quiet сразу после экспоненты в 64-битной версии.
Это звучит немного странно, но на самом деле стандарт говорит (IEEE 754-2008, раздел 6.2.3):
Преобразование тихого NaN из более узкого формата в более широкий формат в том же самом основании, а затем обратно в тот же более узкий формат, не должно изменять тихую полезную нагрузку NaN каким-либо образом, кроме как сделать ее канонической.
Это ничего не говорит о распространении сигнальных NaN. Однако это объясняется разделом 6.2.1.:
Для двоичных форматов полезная нагрузка кодируется в p - 2 младших значащих битах конечного значения поля.
P выше - точность, 24 бит для 32-битного поплавка. Итак, моя ошибка заключалась в использовании сигнальных NaN для полезной нагрузки.
Резюме
Я получил следующие домашние очки:
- использование qNaNs (тихие NaN) поддерживается и поощряется IEEE 754-2008
- Нечетные результаты были связаны с тем, что я пытался использовать sNaN и преобразовывать типы, в результате которых был установлен бит is_quiet
- Оба NumPy и Python действуют в соответствии с IEEE 754 на наиболее распространенных платформах.
- реализация сильно опирается на базовую реализацию C и, таким образом, гарантирует очень мало (в Python есть даже некоторый код, который признает, что NaN не обрабатываются, поскольку они должны быть на некоторых платформах).
- единственный безопасный способ справиться с этим - сделать немного DIY с полезной нагрузкой
Однако есть одна вещь, которая реализована ни в Python, ни в NumPy (ни на каком-либо другом языке, с которым я столкнулся). Раздел 5.12.1:
Языковые стандарты должны обеспечивать необязательное преобразование NaN в поддерживаемом формате в внешние последовательности символов, которые присоединяют к базовым последовательностям символов NaN суффикс, который может представлять полезную нагрузку NaN (см. 6.2). Форма и интерпретация суффикса полезной нагрузки определяются языком. Стандарт языка требует, чтобы любые такие необязательные выходные последовательности принимались в качестве входных данных при преобразовании последовательностей внешних символов в поддерживаемые форматы.