使用が許可されている浮動小数点命令が387である場合、妥当なコストで厳密なIEEE 754セマンティクスを提供することはほぼ不可能です(*)。 FPUを完全な64ビット仮数で動作させ続けて、long double
型を拡張精度で使用できるようにする場合は、特に困難です。通常の「解決策」は、利用可能な唯一の精度で中間計算を実行し、多かれ少なかれ明確に定義された機会に低精度に変換することです。
GCCの最近のバージョンは、Joseph S. Myersが GCCメーリングリストへの2008年の投稿 で示した解釈に従って、中間計算で過剰な精度を処理します。この説明により、gcc -std=c99 -mno-sse2 -mfpmath=387
でコンパイルされたプログラムは、私が理解している限り、最後のビットまで完全に予測可能になります。そして、偶然にそうでない場合、それはバグであり、修正されるでしょう:ジョセフS.マイヤーズの彼の投稿での意図はそれを予測可能にすることです。
Clangが過剰な精度を処理する方法(たとえば、オプション-mno-sse2
が使用される場合)と場所が文書化されていますか?
(*)編集:これは誇張です。 53ビットの仮数を使用するようにx87FPUを構成できる場合、binary64をエミュレートするのは 少し面倒ですがそれほど難しくはありません です。
以下のR ..によるコメントに続いて、これが私の持っているClangの最新バージョンとの短い相互作用のログです:
Hexa:~ $ clang -v
Apple clang version 4.1 (tags/Apple/clang-421.11.66) (based on LLVM 3.1svn)
Target: x86_64-Apple-darwin12.4.0
Thread model: posix
Hexa:~ $ cat fem.c
#include <stdio.h>
#include <math.h>
#include <float.h>
#include <fenv.h>
double x;
double y = 2.0;
double z = 1.0;
int main(){
x = y + z;
printf("%d\n", (int) FLT_EVAL_METHOD);
}
Hexa:~ $ clang -std=c99 -mno-sse2 fem.c
Hexa:~ $ ./a.out
0
Hexa:~ $ clang -std=c99 -mno-sse2 -S fem.c
Hexa:~ $ cat fem.s
…
movl $0, %esi
fldl _y(%rip)
fldl _z(%rip)
faddp %st(1)
movq _x@GOTPCREL(%rip), %rax
fstpl (%rax)
…
これは最初に提起された質問には答えませんが、同様の問題に取り組んでいるプログラマーであれば、この答えが役立つかもしれません。
私は実際に知覚された困難がどこにあるのかわかりません。厳密なIEEE-754binary64セマンティクスを提供する一方で、80387浮動小数点演算に制限され、80ビット長の倍精度計算を保持することは、GCC-4.6.3とclang-3.0(LLVMに基づく)の両方で明確に指定されたC99キャストルールに従っているようです。 3.0)。
追加のために編集:それでも、Pascal Cuoqは正しいです:gcc-4.6.3もclang-llvm-3.0も、 '387浮動小数点に対して実際にこれらのルールを正しく強制しません-ポイント数学。適切なコンパイラオプションが与えられると、ルールはコンパイル時に評価される式に正しく適用されますが、実行時の式には適用されません。以下の休憩の後にリストされている回避策があります。
私は分子動力学シミュレーションコードを実行し、再現性/予測可能性の要件と、可能な場合は可能な限り最高の精度を維持したいという願望に精通しているので、ここで話していることを知っていると主張します。この回答は、ツールが存在し、使いやすいことを示しているはずです。問題は、これらのツールを認識していない、または使用していないことから発生します。
(私が好きな好ましい例は、カハンの加算アルゴリズムです。C99と適切なキャスト(ウィキペディアのサンプルコードなどにキャストを追加する)を使用すると、トリックや追加の一時変数はまったく必要ありません。実装は、コンパイラの最適化レベルに関係なく機能します。 _-O3
_および_-Ofast
_。)
C99は、キャストと割り当ての両方で余分な範囲と精度がすべて削除されることを明示的に示しています(例:5.4.2.2)。これは、計算中に使用される一時変数を_long double
_として定義し、入力変数をその型にキャストすることで、_long double
_演算を使用できることを意味します。 IEEE-754 binary64が必要な場合は、double
にキャストするだけです。
'387に、キャストは上記の両方のコンパイラで割り当てとロードを生成します。これにより、80ビット値がIEEE-754binary64に正しく丸められます。このコストは私の意見では非常に合理的です。かかる正確な時間は、アーキテクチャと周囲のコードによって異なります。通常は、他のコードとインターリーブして、コストを無視できるレベルに下げることができます。 MMX、SSEまたはAVXが使用可能な場合、それらのレジスタは80ビット80387レジスタとは別であり、キャストは通常、値をMMX/SSE/AVXレジスタに移動することによって行われます。
(私は、一時変数に特定の浮動小数点型、たとえばtempdouble
などを使用するプロダクションコードを好みます。これにより、アーキテクチャと速度/精度のトレードオフに応じてdouble
または_long double
_のいずれかに定義できます。)
手短に:
すべての変数とリテラル定数がであるという理由だけで、_
(expression)
_がdouble
の精度であると想定しないでください。double
の精度で結果が必要な場合は、_(double)(expression)
_と記述します。
これは複合式にも当てはまり、キャストのレベルが多い扱いにくい式につながる場合があります。
80ビットの精度で計算したい_expr1
_と_expr2
_があり、それぞれの積を最初に64ビットに丸める必要がある場合は、次を使用します。
_long double expr1;
long double expr2;
double product = (double)(expr1) * (double)(expr2);
_
product
は、2つの64ビット値の積として計算されることに注意してください。 notは80ビットの精度で計算され、切り捨てられます。 80ビットの精度で積を計算してから切り捨てると、次のようになります。
_double other = expr1 * expr2;
_
または、何が起こっているのかを正確に伝える説明的なキャストを追加します。
_double other = (double)((long double)(expr1) * (long double)(expr2));
_
product
とother
はしばしば異なることは明らかです。
C99キャストルールは、32ビット/ 64ビット/ 80ビット/ 128ビットの浮動小数点値が混在している場合に使用する方法を学ぶ必要があるもう1つのツールです。実際、binary32とbinary64のfloat(ほとんどのアーキテクチャではfloat
とdouble
)を混在させると、まったく同じ問題が発生します。
おそらく、キャストルールを正しく適用するためにPascal Cuoqの探索コードを書き直すと、これがより明確になりますか?
_#include <stdio.h>
#define TEST(eq) printf("%-56s%s\n", "" # eq ":", (eq) ? "true" : "false")
int main(void)
{
double d = 1.0 / 10.0;
long double ld = 1.0L / 10.0L;
printf("sizeof (double) = %d\n", (int)sizeof (double));
printf("sizeof (long double) == %d\n", (int)sizeof (long double));
printf("\nExpect true:\n");
TEST(d == (double)(0.1));
TEST(ld == (long double)(0.1L));
TEST(d == (double)(1.0 / 10.0));
TEST(ld == (long double)(1.0L / 10.0L));
TEST(d == (double)(ld));
TEST((double)(1.0L/10.0L) == (double)(0.1));
TEST((long double)(1.0L/10.0L) == (long double)(0.1L));
printf("\nExpect false:\n");
TEST(d == ld);
TEST((long double)(d) == ld);
TEST(d == 0.1L);
TEST(ld == 0.1);
TEST(d == (long double)(1.0L / 10.0L));
TEST(ld == (double)(1.0L / 10.0));
return 0;
}
_
GCCとclangの両方を含む出力は次のとおりです。
_sizeof (double) = 8
sizeof (long double) == 12
Expect true:
d == (double)(0.1): true
ld == (long double)(0.1L): true
d == (double)(1.0 / 10.0): true
ld == (long double)(1.0L / 10.0L): true
d == (double)(ld): true
(double)(1.0L/10.0L) == (double)(0.1): true
(long double)(1.0L/10.0L) == (long double)(0.1L): true
Expect false:
d == ld: false
(long double)(d) == ld: false
d == 0.1L: false
ld == 0.1: false
d == (long double)(1.0L / 10.0L): false
ld == (double)(1.0L / 10.0): false
_
ただし、最近のバージョンのGCCは、_ld == 0.1
_の右側を最初にlong double(つまり、_ld == 0.1L
_)にプロモートし、true
を生成します。また、SSE/AVXの場合、_long double
_は128ビットです。 。
純粋な '387テストでは、
_gcc -W -Wall -m32 -mfpmath=387 -mno-sse ... test.c -o test
clang -W -Wall -m32 -mfpmath=387 -mno-sse ... test.c -o test
_
_...
_、_-fomit-frame-pointer
_、_-O0
_、_-O1
_、_-O2
_、_-O3
_などのさまざまな最適化フラグの組み合わせを_-Os
_として使用します。
他のフラグまたはC99コンパイラを使用すると、_long double
_サイズ(および現在のGCCバージョンの場合は_ld == 1.0
_)を除いて、同じ結果が得られるはずです。何か違いがあったら、聞いていただければ幸いです。そのようなコンパイラ(コンパイラバージョン)についてユーザーに警告する必要があるかもしれません。 MicrosoftはC99をサポートしていないので、私にはまったく興味がないことに注意してください。
Pascal Cuoqは、以下のコメントチェーンで興味深い問題を引き起こしますが、私はすぐには認識しませんでした。
式を評価する場合、GCCと_-mfpmath=387
_のclangの両方で、すべての式が80ビットの精度を使用して評価されるように指定されています。これは、例えばにつながります
_7491907632491941888 = 0x1.9fe2693112e14p+62 = 110011111111000100110100100110001000100101110000101000000000000
5698883734965350400 = 0x1.3c5a02407b71cp+62 = 100111100010110100000001001000000011110110111000111000000000000
7491907632491941888 * 5698883734965350400 = 42695510550671093541385598890357555200 = 100000000111101101101100110001101000010100100001011110111111111111110011000111000001011101010101100011000000000000000000000000
_
バイナリ結果の中央にある1の文字列は、53ビットと64ビットの仮数(それぞれ64ビットと80ビットの浮動小数点数)の差にあるため、誤った結果が生成されます。したがって、期待される結果は
_42695510550671088819251326462451515392 = 0x1.00f6d98d0a42fp+125 = 100000000111101101101100110001101000010100100001011110000000000000000000000000000000000000000000000000000000000000000000000000
_
_-std=c99 -m32 -mno-sse -mfpmath=387
_だけで得られた結果は
_42695510550671098263984292201741942784 = 0x1.00f6d98d0a43p+125 = 100000000111101101101100110001101000010100100001100000000000000000000000000000000000000000000000000000000000000000000000000000
_
理論的には、オプションを使用して、gccとclangに正しいC99丸めルールを適用するように指示できるはずです。
_-std=c99 -m32 -mno-sse -mfpmath=387 -ffloat-store -fexcess-precision=standard
_
ただし、これはコンパイラが最適化する式にのみ影響し、387処理をまったく修正していないようです。あなたが例えばを使用する場合_clang -O1 -std=c99 -m32 -mno-sse -mfpmath=387 -ffloat-store -fexcess-precision=standard test.c -o test && ./test
_で_test.c
_が Pascal Cuoqのサンプルプログラム の場合、IEEE-754ルールに従って正しい結果が得られます-ただし、コンパイラが式を最適化し、全部で387。
簡単に言えば、コンピューティングの代わりに
_(double)d1 * (double)d2
_
gccとclangの両方が実際に '387に計算するように指示します
_(double)((long double)d1 * (long double)d2)
_
これは確かに これはgcc-4.6.3とclang-llvm-3.0の両方に影響を与えるコンパイラのバグであり、簡単に再現できるものだと思います。 (Pascal Cuoqは、_FLT_EVAL_METHOD=2
_は、倍精度引数の演算が常に拡張精度で行われることを意味しますが、それを行うためにlibm
の一部を書き直さなければならないことを除いて、正当な理由はわかりません。 C99では、IEEE-754ルールをハードウェアで実現できます。結局のところ、correct演算は、 '387を変更することで、コンパイラーで簡単に実現できます。式の精度に一致するようにWordを制御します。また、shouldがこの動作を強制するコンパイラオプションが与えられた場合--_-std=c99 -ffloat-store -fexcess-precision=standard
_ --make no _FLT_EVAL_METHOD=2
_の動作が実際に必要かどうかを判断し、後方互換性の問題もありません。)適切なコンパイラフラグが与えられると、コンパイル時に評価される式は正しく評価され、で評価される式のみが正しく評価されることに注意してください。実行時に誤った結果が得られます。
最も簡単な回避策であり、移植性のある回避策は、fesetround(FE_TOWARDZERO)
(_fenv.h
_から)を使用してすべての結果をゼロに丸めることです。
場合によっては、ゼロに向かって丸めることで、予測可能性と病理学的ケースが役立つことがあります。特に、_x = [0,1)
_のような間隔の場合、ゼロに向かって丸めることは、丸めによって上限に到達しないことを意味します。たとえば、評価する場合は重要です。区分的スプライン。
その他の丸めモードでは、387ハードウェアを直接制御する必要があります。
_#include <fpu_control.h>
_から__FPU_SETCW()
を使用するか、オープンコード化することができます。例:_precision.c
_:
_#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define FP387_NEAREST 0x0000
#define FP387_ZERO 0x0C00
#define FP387_UP 0x0800
#define FP387_DOWN 0x0400
#define FP387_SINGLE 0x0000
#define FP387_DOUBLE 0x0200
#define FP387_EXTENDED 0x0300
static inline void fp387(const unsigned short control)
{
unsigned short cw = (control & 0x0F00) | 0x007f;
__asm__ volatile ("fldcw %0" : : "m" (*&cw));
}
const char *bits(const double value)
{
const unsigned char *const data = (const unsigned char *)&value;
static char buffer[CHAR_BIT * sizeof value + 1];
char *p = buffer;
size_t i = CHAR_BIT * sizeof value;
while (i-->0)
*(p++) = '0' + !!(data[i / CHAR_BIT] & (1U << (i % CHAR_BIT)));
*p = '\0';
return (const char *)buffer;
}
int main(int argc, char *argv[])
{
double d1, d2;
char dummy;
if (argc != 3) {
fprintf(stderr, "\nUsage: %s 7491907632491941888 5698883734965350400\n\n", argv[0]);
return EXIT_FAILURE;
}
if (sscanf(argv[1], " %lf %c", &d1, &dummy) != 1) {
fprintf(stderr, "%s: Not a number.\n", argv[1]);
return EXIT_FAILURE;
}
if (sscanf(argv[2], " %lf %c", &d2, &dummy) != 1) {
fprintf(stderr, "%s: Not a number.\n", argv[2]);
return EXIT_FAILURE;
}
printf("%s:\td1 = %.0f\n\t %s in binary\n", argv[1], d1, bits(d1));
printf("%s:\td2 = %.0f\n\t %s in binary\n", argv[2], d2, bits(d2));
printf("\nDefaults:\n");
printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2));
printf("\nExtended precision, rounding to nearest integer:\n");
fp387(FP387_EXTENDED | FP387_NEAREST);
printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2));
printf("\nDouble precision, rounding to nearest integer:\n");
fp387(FP387_DOUBLE | FP387_NEAREST);
printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2));
printf("\nExtended precision, rounding to zero:\n");
fp387(FP387_EXTENDED | FP387_ZERO);
printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2));
printf("\nDouble precision, rounding to zero:\n");
fp387(FP387_DOUBLE | FP387_ZERO);
printf("Product = %.0f\n\t %s in binary\n", d1 * d2, bits(d1 * d2));
return 0;
}
_
Clang-llvm-3.0を使用してコンパイルして実行すると、正しい結果が得られます。
_clang -std=c99 -m32 -mno-sse -mfpmath=387 -O3 -W -Wall precision.c -o precision
./precision 7491907632491941888 5698883734965350400
7491907632491941888: d1 = 7491907632491941888
0100001111011001111111100010011010010011000100010010111000010100 in binary
5698883734965350400: d2 = 5698883734965350400
0100001111010011110001011010000000100100000001111011011100011100 in binary
Defaults:
Product = 42695510550671098263984292201741942784
0100011111000000000011110110110110011000110100001010010000110000 in binary
Extended precision, rounding to nearest integer:
Product = 42695510550671098263984292201741942784
0100011111000000000011110110110110011000110100001010010000110000 in binary
Double precision, rounding to nearest integer:
Product = 42695510550671088819251326462451515392
0100011111000000000011110110110110011000110100001010010000101111 in binary
Extended precision, rounding to zero:
Product = 42695510550671088819251326462451515392
0100011111000000000011110110110110011000110100001010010000101111 in binary
Double precision, rounding to zero:
Product = 42695510550671088819251326462451515392
0100011111000000000011110110110110011000110100001010010000101111 in binary
_
つまり、fp387()
を使用して精度と丸めモードを設定することにより、コンパイラの問題を回避できます。
欠点は、一部の数学ライブラリ(_libm.a
_、_libm.so
_)は、中間結果が常に80ビットの精度で計算されることを前提として記述されている可能性があることです。少なくとも、x86_64のGNU Cライブラリ_fpu_control.h
_にはコメント "libmには拡張精度が必要です"があります。幸い、たとえばGNU Cライブラリから '387実装を取得し、ヘッダーファイルに実装するか、_math.h
_が必要な場合は、既知の動作するlibm
を書き込むことができます。機能性;実際、私はそこで助けることができるかもしれないと思います。
記録のために、以下は私が実験によって見つけたものです。次のプログラムは、Clangでコンパイルしたときのさまざまな動作を示しています。
#include <stdio.h>
int r1, r2, r3, r4, r5, r6, r7;
double ten = 10.0;
int main(int c, char **v)
{
r1 = 0.1 == (1.0 / ten);
r2 = 0.1 == (1.0 / 10.0);
r3 = 0.1 == (double) (1.0 / ten);
r4 = 0.1 == (double) (1.0 / 10.0);
ten = 10.0;
r5 = 0.1 == (1.0 / ten);
r6 = 0.1 == (double) (1.0 / ten);
r7 = ((double) 0.1) == (1.0 / 10.0);
printf("r1=%d r2=%d r3=%d r4=%d r5=%d r6=%d r7=%d\n", r1, r2, r3, r4, r5, r6, r7);
}
結果は最適化レベルによって異なります。
$ clang -v
Apple LLVM version 4.2 (clang-425.0.24) (based on LLVM 3.2svn)
$ clang -mno-sse2 -std=c99 t.c && ./a.out
r1=0 r2=1 r3=0 r4=1 r5=1 r6=0 r7=1
$ clang -mno-sse2 -std=c99 -O2 t.c && ./a.out
r1=0 r2=1 r3=0 r4=1 r5=1 r6=1 r7=1
(double)
とr5
をr6
で区別するキャスト-O2
は、-O0
と、変数r3
とr4
では効果がありません。結果r1
はすべての最適化レベルでr5
と異なりますが、r6
はr3
で-O2
とのみ異なります。