GCC/x86でのlong double
と__float128
の詳細情報を探しています(実際の問題よりも、好奇心から)。
おそらくこれを必要とする人はほとんどいないでしょう(私は初めて、本当にdouble
が必要でした)が、それでも価値があると思います(そして興味深い)ツールボックスに何があるか、そしてそれが何であるかを知ること。
その観点から、ややオープンな質問を許してください。
double
と同じ精度であるか、またはファーストクラスの型として意図されているのかと不平を言う人がいるかもしれません。"long double" site:gcc.gnu.org/onlinedocs
でのGoogle検索では、本当に役立つ情報はあまり得られませんでした。float
だけでなく、8バイトまたは16バイトのメモリが焼き付けられているかどうかを気にする必要はありません...パフォーマンスに大きな影響を与えることなく、double
の代わりにlong double
または__float128
にジャンプするだけですか?long double
タイプはこの問題を解消するはずです。一方、SSEには「拡張精度」などがないため、long double
型は-mfpmath=sse
と相互に排他的であることを理解しています。一方、__float128
は、SSE math(ただし、4倍精度の命令がない場合は、1:1命令ベースではない)で完全に正常に機能するはずです。これらの仮定は正しいですか?(3.と4.は、プロファイリングと逆アセンブルに費やされたいくつかの作業でおそらく理解できますしかし、おそらく他の誰かが以前に同じ考えを持っていて、すでにその作業を行ったことがあります)
背景(これはTL; DR部分です):long double
でDBL_MAX
を検索していたため、最初に<float.h>
を見つけましたが、偶然にLDBL_MAX
が次の行にあります。 「ああ、GCCには実際には128ビットの倍精度があり、それが必要というわけではありませんが、...かっこいい」と最初に考えました。驚き、驚き:sizeof(long double)
は12を返します...待って、16を意味しますか?
CおよびC++標準は、当然のことながら、型の非常に具体的な定義を提供していません。 C99(6.2.5 10)は、double
の数はlong double
のサブセットであると述べていますが、C++ 03は(3.9.1 8)に、long double
は少なくともdouble
(同じことですが、言い方が異なるだけです)と同じ精度であると述べています)。基本的に、long
、int
、short
と同じように、標準はすべてを実装に任せます。
ウィキペディアによると、GCCは「x86プロセッサーで使用される物理ストレージに関係なく、80ビット拡張精度」を使用します。
GCCのドキュメントには、すべて同じページで、タイプのサイズはi386 ABIのために96ビットであると記載されていますが、いずれのオプションでも有効にできるのは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
^^ ^^^^^^^^
これはnotが80ビットの数値であることを示唆しています。 80ビットの数値は18桁の16進数です。 22桁の16進数が変化しているのがわかります。これは、96ビットの数値(24桁の16進数)にかなり似ています。また、0xdeadbeef
は変更されないため、128ビット数ではありません。これは、sizeof
が12を返すのと一致しています。
__int128
の出力は、実際には128ビットの数値のように見えます。最終的にすべてのビットが反転します。
-m128bit-long-double
を使用してコンパイルするとnotになり、ドキュメントに示されているように、long double
を32ビットのゼロパディングで128ビットに揃えます。 __int128
も使用していませんが、実際には値が0x7ffdd000
(?!)でパディングされて128ビットに整列しているようです。
さらに、LDBL_MAX
は、+inf
とlong double
の両方で__float128
として機能するようです。 1.0E100
または1.0E2000
のような数値をLDBL_MAX
に追加したり、__ some_code__から減算したりすると、同じビットパターンになります。
これまでは、foo_MAX
定数はではない最大の表現可能な数を保持する必要があると信じていました(== --- ==)not+inf
(どうやらそうではありません)そうではありませんか?)また、80ビットの数値が128ビットの値に対して+inf
としてどのように機能する可能性があるのかもよくわかりません...多分、結局のところ、私は疲れすぎて何か間違ったことをしました。
広告1。
これらのタイプは、ダイナミックレンジが非常に大きい数値で機能するように設計されています。 long doubleは、x87 FPUにネイティブな方法で実装されています。ハードウェアで計算を行うハードウェアがないため、128b doubleは、現代のx86のソフトウェアモードで実装されると思われます。
面白いことに、多くの浮動小数点演算を連続して行うことは一般的であり、中間結果は実際には宣言された変数に格納されるのではなく、完全な精度を利用してFPUレジスタに格納されます。それが比較の理由です:
double x = sin(0); if (x == sin(0)) printf("Equal!");
安全ではなく、動作を保証できません(追加のスイッチがない場合)。
広告。 3。
使用する精度によって速度に影響があります。次のコマンドを使用して、FPUの精度を変更できます。
void
set_fpu (unsigned int mode)
{
asm ("fldcw %0" : : "m" (*&mode));
}
変数が短いほど速くなり、長いほど遅くなります。 128ビットの倍精度化はおそらくソフトウェアで行われるため、かなり遅くなります。
RAMメモリが無駄になるだけでなく、キャッシュが無駄になることも問題です。64ビットのダブルから80ビットのダブルに移動すると、メモリの33%(32b)からほぼ50%(64b)に無駄になります(キャッシュを含む)。
広告4。
一方、SSEには「拡張精度」などがないため、long double型は-mfpmath = sseと相互に排他的であることを理解しています。一方、__ float128は、SSE mathで完全に正常に機能するはずです(ただし、4倍精度の命令がない場合は、1:1命令ベースではありません)。これらの仮定に基づいていますか?
FPUとSSEユニットは完全に独立しています。SSEと同時にFPUを使用してコードを作成できます。SSEのみを使用するように制約するとコンパイラーは何を生成するのでしょうか?とにかくFPUを使用しようとしていますか?SSEを使用してプログラミングを行っており、GCCはそれ自体で単一のSISDのみを生成します。SIMDバージョンを使用するには、それを支援する必要があります。__float128はおそらく動作しますすべてのマシン、8ビットAVR uCでも、結局ビットをいじっているだけです。
16進数表現の80ビットは、実際には20桁の16進数です。おそらく、使用されていないビットは古い操作のものですか?私のマシンでは、コードをコンパイルし、ロングモードで20ビットのみ変更しました:66b4e0d2-ec09c1d5-00007ffe-deadbeef
128ビットバージョンでは、すべてのビットが変更されています。 objdump
を見ると、ソフトウェアエミュレーションを使用しているかのように見え、FPU命令はほとんどありません。
さらに、LDBL_MAXは、long doubleと__float128の両方で+ infとして機能するようです。 LDBL_MAXに1.0E100や1.0E2000のような数値を加算または減算すると、同じビットパターンになります。これまで、foo_MAX定数は+ infではない最大の表現可能な数を保持するべきだと私は信じていました(明らかにそうではありませんか?)。
これはおかしいようです...
また、80ビットの数値が128ビットの値に対して+ infとしてどのように機能する可能性があるのかもよくわかりません...多分、結局のところ、私は疲れすぎて何か間違ったことをしているのかもしれません。
おそらく拡張されています。 80ビットで+ infと認識されたパターンは、128ビットフロートでも+ infに変換されます。
IEEE-754は、効率的なデータストレージを目的とした32および64の浮動小数点表現と、効率的な計算を目的とした80ビット表現を定義しました。 _float f1,f2; double d1,d2;
_が指定された_d1=f1+f2+d2;
_のようなステートメントは、引数を80ビット浮動小数点値に変換し、それらを追加し、結果を64ビット浮動小数点に戻すことで実行されるという意図でした。タイプ。これには、他の浮動小数点型で直接演算を実行する場合と比較して、3つの利点があります。
32ビット型と64ビット型との間の変換には個別のコードまたは回路が必要ですが、必要なのは、1つの「加算」実装、1つの「乗算」実装、1つの「平方根」実装だけです。等.
まれなケースですが、80ビットの計算タイプを使用すると、他のタイプを直接使用する場合よりもわずかに正確でない結果が生じる可能性があります(他のタイプの計算で511/1024ulpのエラーが発生する場合、最悪の場合の丸めエラーは513/1024ulpです。 )、80ビットのタイプを使用した連鎖計算は、他のタイプを使用した計算よりも、しばしばmuchより正確-な場合があります。
FPUのないシステムでは、計算を実行する前にdouble
を個別の指数と仮数に分離し、仮数を正規化し、個別の仮数と指数をdouble
に変換すると、多少時間がかかります。 1つの計算の結果が別の計算の入力として使用されて破棄される場合、アンパックされた80ビットタイプを使用すると、これらの手順を省略できます。
ただし、浮動小数点演算へのこのアプローチを使用するには、計算で使用されるのと同じ精度で中間結果をコードに格納できるようにする必要があります。たとえば、_temp = d1+d2; d4=temp+d3;
_は、 _d4=d1+d2+d3;
_と同じ結果。私が知ることができることから、_long double
_の目的はbeその型でした。残念ながら、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-float
引数を最も効率的にプロモートできる型として定義された_long double
_もあった場合、_long float
_の意図した目的は有益でした。おそらく48ビット型の浮動小数点ユニットを持たない多くのマシンでは、最適なサイズは32ビット(32ビット演算を直接実行するFPUを備えたマシン)から80(使用するマシン) IEEE-754で想定されている設計)。しかし、今は遅すぎます。
要約すると、4.9999999999999999999999と5.0の差になります。
C99およびC++ 11では、組み込み浮動小数点型のエイリアスである型float_t
およびdouble_t
が追加されました。大まかに言えば、float_t
はfloat
型の値の間で算術を実行した結果の型であり、double_t
はdouble
。