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

Javaには `strictfp` という修飾子があります。`strictfp` によって実際に動作が変わるコードと実行環境を用意してみました。

# マシン

**最近のマシンでJavaを実行する際には `strictfp` は意味を持ちません。**`strictfp` があってもなくてもIEEE準拠の挙動をするはずです。

（なので、 `strictfp` の有無にかかわらずIEEE準拠の挙動をさせようというJEPが出ています： [JEP 306: Restore Always-Strict Floating-Point Semantics](https://openjdk.java.net/jeps/306)）

`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がどうなっているかは確認していません。

* 参考：[What are the system requirements for Java?](https://www.java.com/en/download/help/sysreq.html?ff3)

# プログラム

`strictfp` が意味を持つのは、浮動小数点演算の途中でオーバーフローまたはアンダーフローが起こるような場合、だそうです。`strictfp` が指定された場合はIEEE準拠の動作をし、`strictfp` が指定されなかった場合はオーバーフローまたはアンダーフローが回避されたり、異なる値を返す可能性があります。

* [Java Language Specification - Chapter 15. Expressions](https://docs.oracle.com/javase/specs/jls/se15/html/jls-15.html#jls-15.4)

ここでは、

* `double` 同士の乗算
* `double` 3個の乗算
* `float` 3個の乗算

について、 `strictfp` ありの版となしの版を用意します。そして、特定の値に対するそれらの値を計算させます。3個の乗算に関しては、JITコンパイルが行われることを期待して10万回実行させ、その前後での値を表示させます。

```java
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` は何のために導入され、いかにして不要となったか」みたいな記事を書くかもしれません。~~

書きました：

* [x87 FPUの呪い - Qiita](https://qiita.com/mod_poppo/items/9588b6f425ffe4b5c7bf)