C言語の long double
は環境によって実体がまちまちである。この記事ではその辺をまとめてみる。
C言語の規格での話
まず、C言語の規格で浮動小数点数がどういう扱いになっているか確認しておく。
C言語では、多様な環境に対応するため、浮動小数点数がIEEE 754に準拠しない環境も考慮している。符号、基数、指数部、仮数部がある点はIEEE 754と同じだが、
- 基数 $b$ は1より大きい整数(IEEE 754では2または10のいずれかとしている)
- 指数部の範囲 $\mathit{emin}$, $\mathit{emax}$ の関係は特に規定されていない(IEEE 754では $\mathit{emin}=1-\mathit{emax}$ としている)。
- 0, 無限大、NaNは符号付きでも符号なしでも良い。
- 非正規化数がなくても良い。
となっている。
<float.h>
ではその環境の浮動小数点数のフォーマットにまつわる定数が色々と定義されている。
基数は float
, double
, long double
で共通で、 FLT_RADIX
として定義されている。
精度、指数部の範囲、表せる範囲などは各型ごとのマクロで定義されている(FLT_*
, DBL_*
, LDBL_*
みたいなやつ)。
IEEE 754準拠の場合(Annex F)
現実問題として、その辺に転がっている処理系は大抵何らかの形でIEEE 754に準拠している。なので、C言語の規格のAnnex Fではもう少し突っ込んだ形で(IEEE 754相当の規格であるIEC 60559に言及する形で)浮動小数点数について規定されている。
処理系はこの規定に従っても良いし従わなくても良い。__STDC_IEC_559__
が定義されている場合はAnnex Fに準拠することを表す。
Annex Fでは、
-
float
:単精度(IEEE 754のbinary32) -
double
:倍精度(IEEE 754のbinary64)
と定めているが、 long double
については特定の形式を定めてはおらず、処理系による選択の余地を持たせている。具体的には、 long double
は以下のいずれかであると規定されている:
- 何らかのIEC 60559拡張形式 (double-extended)(推奨)
- 拡張倍精度(double-extended)というのは、仮数部の精度が64ビット以上で、指数部の範囲が-16382〜16383を含むような形式のことである。x87のアレや、四倍精度(binary128)が該当する。
- 非IEC 60559拡張形式
- この場合、精度や範囲は倍精度以上(特に、doubleで表現可能な全ての値を含む)
- IEC 60559 の倍精度(
double
と同じ)
特に、 long double
は非IEEE 754であることが許されている。
(実際、PowerPCの long double
はIEEE 754準拠の形式ではない。後述)
まあ、実際問題として今日使われているメジャーなCコンパイラーでも __STDC_IEC_559__
を定義していなかったりする(Clangとか)ので、あまりAnnex Fがどうのこうの言っても仕方がないのかもしれない。
アーキテクチャごとの事情
アーキテクチャ標準のABIで long double
が規定されている場合がある。また、OSやディストリビューション次第ではアーキテクチャの規定を上書きされている場合がある。
x86系
80ビットの拡張倍精度
x87 FPUは80ビットの拡張倍精度に対応している。指数部は15ビット、仮数部は64ビットとなる。IEEE 754のパラメーターで書くと $b=2$, $p=64$, $\mathit{emax}=16383$ となる。
IEEE 754で定められた交換形式では、2進浮動小数点数のビット列による表現の際は仮数部の先頭の1を省略する、いわゆる「ケチ表現」を使うことになっているが、80ビットのこれはケチ表現を使わない。
(ここから余談)
SSE/SSE2以前のx86では、浮動小数点数を扱う方法がx87 FPUしかなかったため、色々と問題があった。詳しくはJavaの「strictfp
」のようなキーワードでWeb検索すれば出てくると思うが、ここでも簡単に説明しておく。
float
や double
の計算をx87 FPUで普通にやろうとすると、一旦80ビット(仮数部の精度64ビット)で計算してその後それぞれの精度へ変換することになる。そうすると丸めが2回発生するので、IEEE 754準拠の1回丸めの場合と比べて値が変わる可能性がある。FPUのモードをいじって仮数部の精度を変えることもできるが、演算対象の型に応じていちいちモードを変えていたら手間だし、指数部の範囲は広いままとなる。
その結果、処理系やプログラマーは「計算速度はそこそこだが計算結果がマシン依存」か「計算結果はIEEE準拠で再現性があるが、計算速度は遅い」の2択を選ばされることになる。Javaの strictfp
は後者を選択する修飾子である。C言語の場合は FLT_EVAL_METHOD
により処理系がどちらを選んだかわかる(GCCの場合は -fexcess-precision=standard
のようなオプションでどちらを使うか指定できる)。
SSEで float
を、SSE2では double
を型に応じた精度で計算する命令が追加されたので、SSE2以降を対象とするコードで適切なコンパイルオプションを指定すればこの辺の問題はない。特に、x86_64ではSSE2が常に利用可能なので、x86_64をターゲットとする場合はこの辺の面倒な話は忘れて良い。
(ここまで余談)
さて、80ビット(10バイト)というのはメモリに格納する際、中途半端なサイズである。32ビット環境の場合はサイズが4バイトの倍数の方が都合が良いし、64ビット環境では8バイトの倍数の方が都合が良い。
そういうわけで、32ビット環境では80ビット拡張倍精度をメモリを格納する際には2バイトのパディングが入って12バイト、64ビット環境では6バイトのパディングが入って16バイトとなる。メモリ上では128ビット消費することになるが、四倍精度(binary128)とは別物である。
で、実際のところ long double
は拡張倍精度なのか
x86系で long double
と言ったら先述の80ビットの拡張倍精度を指すことが多いが、実際にはそうとは限らない。具体的には、MSVCでは long double
は double
と同じ64ビットの倍精度である。(それから、x86系のAndroidは32ビットでは倍精度、64ビットでは四倍精度らしい。)
GCCではデフォルトでは long double
は80ビットの拡張倍精度だが、コンパイル時のオプションによって long double
の精度を変えることができる。具体的には、
-
-mlong-double-64
:long double
は倍精度(64ビット、仮数部53ビット) -
-mlong-double-80
:long double
は拡張倍精度(80ビット、仮数部64ビット) -
-mlong-double-128
:long double
は四倍精度(128ビット、仮数部113ビット)
となる。
long double
が80ビットの拡張倍精度であっても、先述の理由で sizeof(long double)
は10とはならない。sizeof(long double)
は32ビット環境では12、64ビット環境では16となる。
ちなみに、GCCのオプション -m96bit-long-double
や -m128bit-long-double
は80ビット浮動小数点数型がメモリ上で何バイト消費するか(パディングが何バイトとなるか)を変更するものであり、 long double
の精度を変えるものではない。
ARM
ARM標準のABIで long double
のフォーマットが定められている。
32ビット(AArch32)
long double
は double
と同じく、倍精度である。
64ビット(AArch64)
Procedure Call Standard for the ARM 64-bit Architecture (AArch64)では「long double
は四倍精度」と規定されている。
現行のARMアーキテクチャには四倍精度の命令はないので、 long double
の演算はソフトウェア実装を使うということになる。その代わり、将来のARMが四倍精度に対応した場合はABI互換性を崩さずに移行できる。
ただ、WindowsやiOSではABIの規定を上書きして「long double
は倍精度」としている。
- iOSのABI:ARM64 Function Calling Conventions - developer.apple.com
- Windowsに関しては Overview of ARM64 ABI conventions | Microsoft Docs には特に記述は見当たらなかったが、Microsoft的には
long double
はオワコンだと考えてわざわざ言及しなかったのかもしれない。
MacがARMに移行することが先日発表されたが、Clangのコードを読むと「Darwin」というくくりで long double
を倍精度としているので、順当に行けばARM版Macでも long double
は倍精度となるのではないかと思われる。
Clangのコードの当該部分:
-
AArch64TargetInfo:
long double
は四倍精度-
WindowsARM64TargetInfo:
long double
は倍精度 -
DarwinAArch64TargetInfo:
long double
は倍精度
-
WindowsARM64TargetInfo:
WindowsやiOSがARM標準から乖離する一方、AArch64上のLinuxではARM標準に従って long double
を四倍精度としている。つまり、その辺のラズパイ(3以降)に64ビット版Linuxを入れると、 long double
が四倍精度な環境が出来上がる。
PowerPC
PowerPCは伝統的に long double
をdouble-double演算で実装していたらしい(少なくともLinuxでは。Power Macではデフォルトで long double
は倍精度だったらしい?)。この場合、仮数部の精度は106ビットまたはそれ以上となる。sizeof(long double)
は16バイトとなるが、四倍精度 (binary128) とは別物である。
double-double演算はIEEE準拠の浮動小数点数形式($b$, $p$, $\mathit{emax}$ の3つのパラメーターで表される形式)ではない。なので、色々と扱いづらい。というか処理系での対応もあまり良いとは言えなくて、GCCが定数畳み込みに対応していないらしい:
一方、最近のPower ISA(Power ISA 3.0)では、本物の四倍精度 (binary128) に対応しているようだ。かといっていきなり long double
を本物の四倍精度にすると既存のバイナリとのABI互換性がなくなってしまう。そのため、四倍精度を扱いたい場合は __float128
や _Float128
のような専用の型を使うことになる。
ABI互換性が壊れてもいいから long double
を四倍精度にしたい、という場合はGCCに -mabi=ieeelongdouble
を指定する。ABIが変わるので、既存のビルド済みlibcとリンクしようとするとエラーが出る(libcのビルドからやり直したくないという場合は、 -Wl,-no-warn-mismatch
を指定する)。
PPC64LE上のFedoraでは long double
を四倍精度にする(-mabi=ieeelongdouble
をデフォルトにする)という計画があるらしい。参考:
- The saga of the Power ISA 128-bit long double
- Changes/PPC64LE Float128 Transition - Fedora Project Wiki
まとめると、GCCのオプションと long double
の精度、各環境でのデフォルトの関係は
-
-mlong-double-64
:long double
は倍精度(Macでのデフォルト) -
-mlong-double-128 -mabi=ibmlongdouble
:long double
はdouble-double演算(現行のLinux等でのデフォルト) -
-mlong-double-128 -mabi=ieeelongdouble
:long double
は四倍精度(将来のPPC64LE上でのFedoraでこれをデフォルトにしようという計画がある)
となる。
各コンパイラーでの事情
GCC
先に述べたように、デフォルトでは
- x86系:80ビット拡張倍精度
- ARM:倍精度または四倍精度(プラットフォームのABIに従う)
- PowerPC:double-double(倍々精度、擬似四倍精度)
である。しかし、x86系とPowerPCについては、オプションで変更できるようになっている。
Clang
基本的にはGCCと同様だと思う。
LLVM/Clangはソースコードが比較的わかりやすいので、直接ソースコードを見て確認するのが早いかもしれない。clang/lib/Basic/Targets/ 以下を LongDoubleFormat
で検索すると色々出てくる:
https://github.com/search?q=LongDoubleFormat+repo%3Allvm%2Fllvm-project++path%3Aclang%2Flib%2FBasic%2FTargets%2F&type=Code&ref=advsearch&l=&l=
Microsoft Visual C++
double
と同じ、倍精度(64ビット)である。x86_64の場合は、 double
と同じくxmmレジスターを使って受け渡しする。
Intel C++ Compiler
Intel C++ Compilerは、基本的にWindowsではMSVC互換、それ以外ではGCC互換として振る舞う。long double
も例外に漏れず、WindowsではMSVCと同様の倍精度で、Unix系では80ビットの拡張倍精度となる。
WindowsおよびLinux上では、GCCと同様に、コマンドラインオプションで long double
の定義を上書きできるようだ:
-
/Qlong-double:Windows上で
long double
を拡張倍精度(80ビット)にする -
-mlong-double-N:Linux上で
long double
を倍精度(N=64)または拡張倍精度(N=80; デフォルト)または四倍精度(たぶん。N=128)とする
筆者はICCを触れる環境にないので、これらを実際に試したわけではない。
まとめ
よくある環境では、 long double
は
- 倍精度(
double
と同じ) - 80ビットの拡張倍精度
-
sizeof(long double)
は12または16となる。
-
- 四倍精度
-
sizeof(long double)
は16となる。 - ソフトウェア実装となる場合がある。
-
- double-double演算(倍々精度、擬似四倍精度)
-
sizeof(long double)
は16となる。
-
のいずれかの可能性がある。もっとマイナーな環境では、ここに挙げたもの以外の形式の可能性がある。
long double
の注意点としては、
-
sizeof(long double) == 16
だからと言って四倍精度(仮数部113ビット)とは限らない - 環境によってはソフトウェア実装による四倍精度演算が利用されるので、
double
と比べて大幅に遅くなる場合がある
などがある。
いずれにせよ、環境によって long double
の精度はまちまちなので、異なるマシン間で再現性のある数値計算をしたい場合は long double
は避けるべきである。