LoginSignup
3

More than 3 years have passed since last update.

Javaの strictfp が実際に意味を持つ環境を用意する

Last updated at Posted at 2020-10-21

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の例では double0x1p-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の例では float0x1p-100 * 0x1p-100 * 0x1p100 を計算させて見ました。途中結果の 0x1p-200float では表現できないので、最終結果は 0 となるはずです。実際そうなっています。こちらは、JITコンパイルさせても結果が変わるようなことはありませんでした。

ちなみに、Intel SDEに -quark を指定して実行した場合は無印のPentium相当となるようで、Javaが対応しておらず Executed instruction not valid for specified chip (PENTIUM): 0xf7f61dd0: nop ebx, edi で落ちました。

ここでは深い解説はしません

ここでは深い解説はしません。

そのうち「x87 FPUの呪い」だとか「Javaの strictfp は何のために導入され、いかにして不要となったか」みたいな記事を書くかもしれません。

書きました:

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
3