Длинный двойной (GCC специфический) и __float128
Я ищу подробную информацию о long double
и __float128
в GCC/x86 (больше из любопытства, чем из-за реальной проблемы).
Мало кто, возможно, когда-нибудь понадобится для этого (мне просто в первый раз действительно нужен был double
), но я думаю, что все же стоит (и интересно) знать, что у вас есть на панели инструментов и что это значит.
В этом свете, пожалуйста, извините мои несколько открытые вопросы:
- Может ли кто-нибудь объяснить обоснование реализации и предполагаемое использование этих типов, также в сравнении друг с другом? Например, являются ли они "реализациями смущения", потому что стандарт допускает тип, и кто-то может жаловаться, если они имеют только ту же точность, что и
double
, или они предназначены для первоклассных типов?
- В качестве альтернативы, у кого-то есть хорошая, полезная веб-ссылка для совместного использования? Поиск Google в
"long double" site:gcc.gnu.org/onlinedocs
не дал мне много полезного.
- Предполагая, что общая мантра "если вы считаете, что вам нужно удвоить, вы, вероятно, не понимаете, с плавающей точкой", не применяется, то есть вам действительно нужна более высокая точность, чем просто
float
, и все равно, 8 или 16 байт памяти сжигаются... разумно ли ожидать, что можно просто перейти на long double
или __float128
вместо double
без значительного воздействия на производительность?
- Функция расширенной точности процессоров Intel исторически была источником неприятных сюрпризов, когда значения перемещались между памятью и регистрами. Если на самом деле хранится 96 бит, тип
long double
должен устранить эту проблему. С другой стороны, я понимаю, что тип long double
является взаимоисключающим с -mfpmath=sse
, поскольку в SSE нет такой вещи, как "расширенная точность". __float128
, с другой стороны, должен отлично работать с математикой SSE (хотя в отсутствие инструкций с четкой точностью, конечно, не на базе инструкций 1:1). Правильно ли я в этих предположениях?
(3 и 4. возможно, можно понять с некоторой работой, потраченной на профилирование и разборку, но, возможно, кто-то еще думал об этом ранее и уже сделал эту работу.)
Фон (это часть TL; DR):
Я сначала наткнулся на long double
, потому что я искал DBL_MAX
в <float.h>
, а случайно LDBL_MAX
- на следующей строке. "О, посмотри, у GCC на самом деле есть 128 бит в два раза, а не то, что они мне нужны, но... круто" была моей первой мыслью. Сюрприз, сюрприз: sizeof(long double)
возвращает 12... подождите, вы имеете в виду 16?
Стандарты C и С++ неудивительно не дают очень конкретного определения типа. C99 (6.2.5 10) говорит, что числа double
являются подмножеством long double
, тогда как С++ 03 утверждает (3.9.1 8), что long double
имеет как минимум такую же точность, как double
(что это одно и то же, только по-разному). В принципе, стандарты оставляют все для реализации так же, как с long
, int
и short
.
В Википедии говорится, что GCC использует "80-битную расширенную точность для процессоров x86 независимо от используемого физического хранилища".
В документации GCC указано все на той же странице, что размер этого типа составляет 96 бит из-за i386 ABI, но не более 80 бит точности разрешены любой опцией (да? какой?), также Pentium и более новые процессоры хотят, чтобы они были выровнены как 128-битные числа. Это значение по умолчанию составляет 64 бит и может быть включено вручную под 32 битами, что приводит к 32-разрядному нулевому заполнению.
Время выполнения теста:
#include <stdio.h>
#include <cfloat>
int main()
{
#ifdef USE_FLOAT128
typedef __float128 long_double_t;
#else
typedef long double long_double_t;
#endif
long_double_t ld;
int* i = (int*) &ld;
i[0] = i[1] = i[2] = i[3] = 0xdeadbeef;
for(ld = 0.0000000000000001; ld < LDBL_MAX; ld *= 1.0000001)
printf("%08x-%08x-%08x-%08x\r", i[0], i[1], i[2], i[3]);
return 0;
}
Результат при использовании long double
выглядит примерно так: с неизменными отмеченными цифрами, а все остальные в конечном итоге меняются по мере увеличения и увеличения числа:
5636666b-c03ef3e0-00223fd8-deadbeef
^^ ^^^^^^^^
Это говорит о том, что это не 80-битное число. 80-битное число имеет 18 шестнадцатеричных цифр. Я вижу 22 шестнадцатеричных цифры, которые выглядят намного больше, чем 96-битное число (24 шестнадцатеричных разряда). Он также не является 128-битным числом, так как 0xdeadbeef
не затрагивается, что согласуется с sizeof
, возвращающим 12.
Выход для __int128
выглядит как просто 128-битное число. Все биты в конце концов перевернуты.
Компиляция с -m128bit-long-double
делает не выравнивание long double
до 128 бит с 32-разрядным нулевым заполнением, как указано в документации. Он также не использует __int128
, но, по-видимому, выравнивается до 128 бит, заполняя его значением 0x7ffdd000
(?!).
Кроме того, LDBL_MAX
работает как +inf
для long double
и __float128
. Добавление или вычитание числа, такого как 1.0E100
или 1.0E2000
в/из LDBL_MAX
, приводит к тому же битовому шаблону.
До сих пор я полагал, что константы foo_MAX
должны были содержать наибольшее представимое число, которое не является +inf
(по-видимому, это не так?). Я также не совсем уверен, как 80-битное число могло бы действовать как +inf
для 128-битного значения... возможно, я просто слишком устал в конце дня и сделал что-то не так.
Ответы
Ответ 1
Объявление 1.
Эти типы предназначены для работы с числами с огромным динамическим диапазоном. Длинный двойной вариант реализован на родном пути в FPU x87. Двойной подозреваемый 128b будет реализован в программном режиме на современных x86, так как нет оборудования для выполнения вычислений на оборудовании.
Самое смешное, что довольно часто приходится выполнять много операций с плавающей запятой в строке, а промежуточные результаты фактически не хранятся в объявленных переменных, а хранятся в регистрах FPU, используя полную точность. Вот почему сравнение:
double x = sin(0); if (x == sin(0)) printf("Equal!");
Не безопасно и не может быть гарантировано работать (без дополнительных переключателей).
Ad. 3.
Влияние на скорость зависит от того, какую точность вы используете. Вы можете изменить используемую точность FPU, используя:
void
set_fpu (unsigned int mode)
{
asm ("fldcw %0" : : "m" (*&mode));
}
Это будет быстрее для более коротких переменных, медленнее дольше. 128 бит удваивается, вероятно, будет сделано в программном обеспечении, поэтому будет намного медленнее.
Это не только о RAM-памяти впустую, а о том, что кеш пропал впустую. Переход к 80-битовому удвоению с 64-битной двойной будет отниматься с 33% (32b) до почти 50% (64b) памяти (включая кеш).
Объявление 4.
С другой стороны, я понимаю, что длинный двойной тип взаимно эксклюзив с -mfpmath = sse, поскольку нет такой вещи, как "расширенный точности" в SSE. С другой стороны, __float128 должен работать просто отлично справляется с математикой SSE (хотя в отсутствие четкости инструкции, конечно, не на базе инструкции 1:1). Я прямо под эти предположения?
Блоки FPU и SSE полностью разделены. Вы можете писать код с помощью FPU одновременно с SSE. Вопрос в том, что будет генерировать компилятор, если вы ограничиваете его использование только SSE? Будет ли она пытаться использовать FPU? Я занимаюсь программированием с SSE, и GCC будет генерировать только одиночный SISD самостоятельно. Вы должны помочь ему использовать SIMD-версии. __float128, вероятно, будет работать на каждой машине, даже 8-битный AVR UC. В конце концов, это просто игра с битами.
80-битное шестнадцатеричное представление на самом деле составляет 20 шестнадцатеричных цифр. Может быть, биты, которые не используются, - это какая-то старая операция? На моей машине я скомпилировал ваш код, и только 20 бит изменяются длинными
режим: 66b4e0d2-ec09c1d5-00007ffe-deadbeef
В 128-битной версии все биты изменяются. Глядя на objdump
, похоже, что он использует эмуляцию программного обеспечения, инструкции FPU почти отсутствуют.
Кроме того, LDBL_MAX, похоже, работает как + inf для длинных двойных и __float128. Добавление или вычитание числа, такого как 1.0E100 или 1.0E2000 в/из LDBL_MAX, приводит к тому же битовому шаблону. До сих пор это был мой что константы foo_MAX должны были держать число, которое не является + inf (по-видимому, это не дело?).
Это кажется странным...
Я также не совсем уверен, как возможно 80-битное число действовать как + inf для 128-битного значения... может быть, я просто слишком устал в конце и сделали что-то не так.
Вероятно, он расширяется. Паттерн, признанный как + inf в 80-битном, переводится на + inf в 128-битный float.
Ответ 2
IEEE-754 определил представления 32 и 64 с плавающей запятой для эффективного хранения данных и 80-битное представление для эффективного вычисления. Предполагалось, что при задании float f1,f2; double d1,d2;
оператор, подобный d1=f1+f2+d2;
, будет выполнен путем преобразования аргументов в 80-битные значения с плавающей запятой, добавления их и преобразования результата обратно в 64-разрядный тип с плавающей запятой. Это будет иметь три преимущества по сравнению с выполнением операций с другими типами с плавающей запятой напрямую:
-
Если для конверсий в/из 32-разрядных и 64-разрядных типов потребуется отдельный код или схема, необходимо будет иметь только одну реализацию "добавить", одну "многократно" реализацию, одну реализация "квадратного корня" и т.д.
-
Хотя в редких случаях использование 80-разрядного вычислительного типа может дать результаты, которые были немного менее точными, чем непосредственно с использованием других типов (наихудшая ошибка округления равна 513/1024ulp в случаях, когда вычисления на других типах ошибка 511/1024ulp), скопированные вычисления с использованием 80-битных типов часто бывают более точными - иногда гораздо точнее - чем вычисления с использованием других типов.
-
В системе без FPU разделение a double
на отдельный показатель и мантисса перед выполнением вычислений, нормализация мантиссы и преобразование отдельной мантиссы и экспонента в double
, отнимают много времени. Если результат одного вычисления будет использоваться как вход для другого и отброшен, использование распакованного 80-битного типа позволит пропустить эти шаги.
Для того чтобы этот подход к математике с плавающей запятой был полезен, однако, крайне важно, чтобы код мог хранить промежуточные результаты с той же точностью, что и при вычислении, так что temp = d1+d2; d4=temp+d3;
будет давать тот же результат, что и d4=d1+d2+d3;
. Из того, что я могу сказать, целью long double
был такой тип. К сожалению, несмотря на то, что K & R сконструирован C, так что все значения с плавающей точкой будут переданы вариационным методам таким же образом, ANSI C сломал это. В C, как первоначально было разработано, с учетом кода float v1,v2; ... printf("%12.6f", v1+v2);
, метод printf
не должен был бы беспокоиться о том, будет ли v1+v2
давать float
или double
, поскольку результат будет принудительно принят к известному тип независимо. Кроме того, даже если тип v1
или v2
изменился на double
, оператор printf
не изменился бы.
ANSI C, однако, требует, чтобы код, который вызывает printf
, должен знать, какие аргументы double
и которые long double
; много кода - если не большинство - кода, который использует long double
, но был написан на платформах, где он синонимом double
не использует правильные спецификации формата для значений long double
. Вместо того, чтобы long double
быть 80-битным типом, кроме тех случаев, когда он передавался как аргумент вариационного метода, в этом случае он был бы принудительно принят до 64 бит, многие компиляторы решили сделать long double
синонимом double
и не предлагать никаких средства хранения результатов промежуточных вычислений. Поскольку использование расширенного типа точности для вычислений полезно только в том случае, если этот тип становится доступным для программиста, многие люди пришли к выводу, что расширенная точность является злой, хотя только ANSI C неспособно обрабатывать вариативные аргументы разумно, что сделало его проблематичным.
PS. Целевая цель long double
выиграла бы, если бы существовал long float
, который был определен как тип, к которому аргументы float
могли быть наиболее эффективно продвинуты; на многих машинах без блоков с плавающей точкой, которые, вероятно, были бы 48-битным типом, но оптимальный размер мог бы варьироваться от 32 бит (на машинах с FPU, который напрямую выполняет 32-битную математику) до 80 (на машинах, которые используют дизайн, предусмотренный IEEE-754). Слишком поздно, однако.
Ответ 3
Это сводится к разнице между 4.9999999999999999999 и 5.0.
- Хотя диапазон является основным отличием, важна точность.
- Эти типы данных понадобятся при вычислении больших кругов или координатной математике, которая, вероятно, будет использоваться с системами GPS.
- Поскольку точность намного лучше, чем нормальная двойная, это означает, что вы можете сохранить обычно 18 значащих цифр без потери точности вычислений.
- Расширенная точность, по-моему, использует 80 бит (используется в основном в математических процессорах), поэтому 128 бит будут намного точнее.