Javaには strictfp
という修飾子があります。strictfp
によって実際に動作が変わるコードと実行環境を用意してみました。
マシン
最近のマシンでJavaを実行する際には strictfp
は意味を持ちません。strictfp
があってもなくてもIEEE準拠の挙動をするはずです。
(なので、 strictfp
の有無にかかわらずIEEE準拠の挙動をさせようというJEPが出ています: JEP 306: Restore Always-Strict Floating-Point Semantics)
strictfp
が意味を持つのはx86系の、SSE2が実装されていないCPUです。Intelの場合は2000年ごろのPentium 4以降でSSE2が実装されているので、SSE2がないマシンを用意しようとするとそれよりも前のマシンが必要になります。
ですが、20年以上前のマシンを用意するのは難しいですよね(筆者はレトロPCマニアではありません)。そこで、エミュレーションを利用します。ここではIntel SDEとQEMUの2つのエミュレーターを考えます。
Intel SDEで指定できるCPUの種類の中で、SSE2が実装されていないものとしては -quark
があるようです。その次に機能が少なそうなのが -p4
つまりPentium 4なので、Intel SDEでSSE2のない環境を用意しようと思ったら -quark
一択です。
QEMUでは -cpu pentium
, -cpu pentium2
, -cpu pentium3
などが利用できます。Linux上ではuser space emulationと言って仮想マシンを用意しなくてもプログラムを動かせるので便利です。この記事でもLinuxを使います。
JVMは、Oracleが配布しているJava 8 Update 261を利用します。こいつはPentium II以降に対応しているようです。新しめのJREがどうなっているかは確認していません。
プログラム
strictfp
が意味を持つのは、浮動小数点演算の途中でオーバーフローまたはアンダーフローが起こるような場合、だそうです。strictfp
が指定された場合はIEEE準拠の動作をし、strictfp
が指定されなかった場合はオーバーフローまたはアンダーフローが回避されたり、異なる値を返す可能性があります。
ここでは、
-
double
同士の乗算 -
double
3個の乗算 -
float
3個の乗算
について、 strictfp
ありの版となしの版を用意します。そして、特定の値に対するそれらの値を計算させます。3個の乗算に関しては、JITコンパイルが行われることを期待して10万回実行させ、その前後での値を表示させます。
class StrictfpTest
{
static double multiplyDefault(double x, double y)
{
return x * y;
}
static strictfp double multiplyStrict(double x, double y)
{
return x * y;
}
static double multiplyThreeDoublesDefault(double x, double y, double z)
{
return x * y * z;
}
static strictfp double multiplyThreeDoublesStrict(double x, double y, double z)
{
return x * y * z;
}
static float multiplyThreeFloatsDefault(float x, float y, float z)
{
return x * y * z;
}
static strictfp float multiplyThreeFloatsStrict(float x, float y, float z)
{
return x * y * z;
}
public static void main(String[] args)
{
{
double x = 0x1.00002fff0p0, y = 0x1.000000008p0;
System.out.printf("multiplyDefault(%a, %a) = %a\n", x, y, multiplyDefault(x, y));
System.out.printf("multiplyStrict(%a, %a) = %a\n", x, y, multiplyStrict(x, y));
}
{
double x = 0x1.fffe0effffffep0, y = 0x1.0000000000001p0;
System.out.printf("multiplyDefault(%a, %a) = %a\n", x, y, multiplyDefault(x, y));
System.out.printf("multiplyStrict(%a, %a) = %a\n", x, y, multiplyStrict(x, y));
}
{
double x = 0x1.fffe0effffffep-51, y = 0x1.0000000000001p-1000;
System.out.printf("multiplyDefault(%a, %a) = %a\n", x, y, multiplyDefault(x, y));
System.out.printf("multiplyStrict(%a, %a) = %a\n", x, y, multiplyStrict(x, y));
}
{
double x = 0x1p-1000, y = 0x1p-1000, z = 0x1p1000;
System.out.printf("multiplyThreeDoublesDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesDefault(x, y, z));
System.out.printf("multiplyThreeDoublesStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesStrict(x, y, z));
for (int i = 0; i < 100000; ++i) {
multiplyThreeDoublesDefault(x, z, y);
multiplyThreeDoublesStrict(x, z, y);
}
System.out.printf("multiplyThreeDoublesDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesDefault(x, y, z));
System.out.printf("multiplyThreeDoublesStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeDoublesStrict(x, y, z));
}
{
float x = 0x1p-100f, y = 0x1p-100f, z = 0x1p100f;
System.out.printf("multiplyThreeFloatsDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsDefault(x, y, z));
System.out.printf("multiplyThreeFloatsStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsStrict(x, y, z));
for (int i = 0; i < 1000000; ++i) {
multiplyThreeFloatsDefault(x, z, y);
multiplyThreeFloatsStrict(x, z, y);
}
System.out.printf("multiplyThreeFloatsDefault(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsDefault(x, y, z));
System.out.printf("multiplyThreeFloatsStrict(%a, %a, %a) = %a\n", x, y, z, multiplyThreeFloatsStrict(x, y, z));
}
}
}
まず、モダンな環境での実行結果は次の通りです。筆者はx86_64で実行しましたが、SSE2を持つx86系プロセッサーや、AArch64などのCPUであれば同じ結果となるはずです。
$ java StrictfpTest
multiplyDefault(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0
multiplyStrict(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0
multiplyDefault(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0
multiplyStrict(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0
multiplyDefault(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff07p-1022
multiplyStrict(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff07p-1022
multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
モダンな環境では strictfp
ありなしで結果は変わらないことが見て取れます。JITコンパイルの前後で値が変わるようなこともありません。
次に、QEMUを使ってPentium IIで実行させます。~/jre1.8.0_261/bin/java
に32ビット版のJavaコマンドを置きました。
$ qemu-i386 -cpu pentium2 ~/jre1.8.0_261/bin/java StrictfpTest
multiplyDefault(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0
multiplyStrict(0x1.00002fffp0, 0x1.000000008p0) = 0x1.00002fff80001p0
multiplyDefault(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0
multiplyStrict(0x1.fffe0effffffep0, 0x1.0000000000001p0) = 0x1.fffe0fp0
multiplyDefault(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff08p-1022
multiplyStrict(0x1.fffe0effffffep-51, 0x1.0000000000001p-1000) = 0x0.0000000ffff07p-1022
multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeDoublesDefault(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x1.0p-1000
multiplyThreeDoublesStrict(0x1.0p-1000, 0x1.0p-1000, 0x1.0p1000) = 0x0.0p0
multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
multiplyThreeFloatsDefault(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
multiplyThreeFloatsStrict(0x1.0p-100, 0x1.0p-100, 0x1.0p100) = 0x0.0p0
まず、最初の例 0x1.00002fffp0 * 0x1.000000008p0
ではオーバーフローもアンダーフローも起こらないため、strictfp
の有無で結果は変わりません。次の例 0x1.fffe0effffffep0 * 0x1.0000000000001p0
でも同様です。
一方、第3の例 0x1.fffe0effffffep-51 * 0x1.0000000000001p-1000
ではアンダーフローが起こり、結果が非正規化数となります。そして、 strictfp
の有無で最後の桁が1ずれています。IEEE 754準拠なのはもちろん strictfp
をつけた方で、この場合は真の値に近いのも strictfp
をつけた方です。
第4の例では double
で 0x1p-1000 * 0x1p-1000 * 0x1p1000
($2^{-1000} \times 2^{-1000} \times 2^{1000}$)を計算しています。途中結果の 0x1p-2000
($2^{-2000}$)は double
では指数部が小さ過ぎて表現できないので 0
となり、最終結果も 0
となるはずです。実際、strictfp
をつけた方や、JITコンパイルする前の方は 0x0.0p0
を返しています。
しかし、 strictfp
をつけなかった方は、JITコンパイル後の結果が 0x1p-1000
となっています。これは、指数部が本来の double
よりも広い範囲で途中計算が行われたことを意味します。
第5の例では float
で 0x1p-100 * 0x1p-100 * 0x1p100
を計算させて見ました。途中結果の 0x1p-200
は float
では表現できないので、最終結果は 0
となるはずです。実際そうなっています。こちらは、JITコンパイルさせても結果が変わるようなことはありませんでした。
ちなみに、Intel SDEに -quark
を指定して実行した場合は無印のPentium相当となるようで、Javaが対応しておらず Executed instruction not valid for specified chip (PENTIUM): 0xf7f61dd0: nop ebx, edi
で落ちました。
ここでは深い解説はしません
ここでは深い解説はしません。
そのうち「x87 FPUの呪い」だとか「Javaの strictfp
は何のために導入され、いかにして不要となったか」みたいな記事を書くかもしれません。
書きました: