LoginSignup
67
35

More than 3 years have passed since last update.

long doubleの話

Posted at

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検索すれば出てくると思うが、ここでも簡単に説明しておく。

floatdouble の計算を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 doubledouble と同じ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 doubledouble と同じく、倍精度である。

64ビット(AArch64)

Procedure Call Standard for the ARM 64-bit Architecture (AArch64)では「long double は四倍精度」と規定されている。

現行のARMアーキテクチャには四倍精度の命令はないので、 long double の演算はソフトウェア実装を使うということになる。その代わり、将来のARMが四倍精度に対応した場合はABI互換性を崩さずに移行できる。

ただ、WindowsやiOSではABIの規定を上書きして「long double は倍精度」としている。

MacがARMに移行することが先日発表されたが、Clangのコードを読むと「Darwin」というくくりで long double を倍精度としているので、順当に行けばARM版Macでも long double は倍精度となるのではないかと思われる。

Clangのコードの当該部分:

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 をデフォルトにする)という計画があるらしい。参考:

まとめると、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 は避けるべきである。

67
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67
35