Подтвердить что ты не робот

Длинный двойной (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-битного значения... возможно, я просто слишком устал в конце дня и сделал что-то не так.

4b9b3361

Ответ 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 бит будут намного точнее.