Préparez un environnement où Java strictfp a vraiment du sens

Java a un modificateur appelé strictfp. J'ai préparé l'environnement de code et d'exécution dont le comportement change réellement en fonction de strictfp.

Machine

** strictfp n'a aucune signification lors de l'exécution de Java sur des machines modernes. ** Doit se comporter selon IEEE avec ou sans strictfp.

(Par conséquent, il existe un JEP qui essaie de rendre le comportement conforme IEEE avec ou sans strictfp: JEP 306: Restore Always-Strict Floating-Point Semantics / 306)))

strictfp est significatif pour les processeurs x86 sur lesquels SSE2 n'est pas implémenté. Dans le cas d'Intel, SSE2 est implémenté dans Pentium 4 ou version ultérieure vers 2000, donc si vous souhaitez préparer une machine sans SSE2, vous avez besoin d'une machine antérieure.

Cependant, il est difficile de préparer une machine de plus de 20 ans (je ne suis pas un passionné de PC rétro). Par conséquent, nous utiliserons l'émulation. Considérez deux émulateurs, Intel SDE et QEMU.

Parmi les types de processeurs pouvant être spécifiés par Intel SDE, il semble y avoir «-quark» qui n'implémente pas SSE2. La moindre fonctionnalité suivante est -p4, c'est-à-dire Pentium 4, donc si vous voulez préparer un environnement sans SSE2 avec Intel SDE, vous pouvez choisir -quark.

Vous pouvez utiliser -cpu pentium, -cpu pentium2, -cpu pentium3, etc. dans QEMU. Sous Linux, c'est pratique car vous pouvez exécuter le programme sans préparer une machine virtuelle appelée émulation d'espace utilisateur. Cet article utilise également Linux.

La JVM utilise Java 8 Update 261 distribué par Oracle. Ce type semble être compatible avec Pentium II ou plus tard. Je n'ai pas confirmé à quoi ressemble le nouveau JRE.

programme

Il semble que strictfp soit significatif lorsqu'un dépassement de capacité ou de sous-capacité se produit au milieu d'une opération en virgule flottante. Si strictfp est spécifié, il se comporte d'une manière conforme à IEEE, et si strictfp n'est pas spécifié, un dépassement de capacité ou un dépassement inférieur peut être évité ou une valeur différente peut être renvoyée.

ici,

Pour, nous préparerons une version avec et sans strictfp. Laissez-les ensuite calculer ces valeurs pour des valeurs spécifiques. Pour 3 multiplications, exécutez 100 000 fois dans l'espoir que la compilation JIT sera effectuée et affichez les valeurs avant et après cela.

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));
        }
    }
}

Tout d'abord, le résultat de l'exécution dans l'environnement moderne est le suivant. Je l'ai exécuté sur x86 \ _64, mais si vous avez un processeur x86 avec SSE2 ou un processeur tel que AArch64, vous devriez obtenir le même résultat.

$ 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

On peut voir que dans un environnement moderne, les résultats ne changent pas avec ou sans strictfp. La valeur ne change pas avant et après la compilation JIT.

Ensuite, utilisez QEMU pour l'exécuter sur Pentium II. J'ai mis la version 32 bits de la commande Java dans ~ / jre1.8.0_261 / bin / 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

Tout d'abord, le premier exemple 0x1.00002fffp0 * 0x1.000000008p0 ne provoque pas de débordement ou de sous-dépassement, donc le résultat ne change pas avec ou sans strictfp. La même chose est vraie pour l'exemple suivant 0x1.fffe0effffffep0 * 0x1.0000000000001p0.

D'autre part, dans le troisième exemple «0x1.fffe0effffffep-51 * 0x1.0000000000001p-1000», un dépassement inférieur se produit et le résultat est un nombre non normalisé. Et le dernier chiffre est décalé de 1 selon la présence ou l'absence de strictfp. Bien sûr, c'est celui avec «strictfp» qui est conforme à la norme IEEE 754, et dans ce cas, celui avec «strictfp» qui est proche de la vraie valeur.

Dans le quatrième exemple, double est utilisé pour calculer 0x1p-1000 * 0x1p-1000 * 0x1p1000 ($ 2 ^ {-1000} \ times 2 ^ {-1000} \ times 2 ^ {1000} ). Le résultat intermédiaire «0x1p-2000» ( 2 ^ {-2000} $) doit être «0» car la partie exposant est trop petite pour être exprimée en «double», et le résultat final doit également être «0». En fait, ceux avec strictfp et ceux avant la compilation JIT retournent 0x0.0p0.

Cependant, si vous n'ajoutez pas strictfp, le résultat après la compilation JIT est 0x1p-1000. Cela signifie que la partie exposant a été calculée dans une plage plus large que le «double» d'origine.

Dans le cinquième exemple, j'ai essayé de calculer 0x1p-100 * 0x1p-100 * 0x1p100 avec float. Le résultat intermédiaire «0x1p-200» ne peut pas être représenté par «float», donc le résultat final doit être «0». En fait, ça l'est. Ici, la compilation JIT n'a pas changé le résultat.

À propos, quand il est exécuté en spécifiant -quark dans Intel SDE, il semble être équivalent à Pentium non marqué, Java ne prend pas en charge ʻInstruction exécutée non valide pour la puce spécifiée (PENTIUM): 0xf7f61dd0: nop ebx, Je suis tombé dans edi`.

Je ne donnerai pas une explication approfondie ici

Je ne donnerai pas ici une explication approfondie.

Vous pourriez écrire des articles comme "x87 FPU Curse" ou "Pourquoi Java's strictfp a-t-il été introduit et comment il n'était plus nécessaire?"

Recommended Posts

Préparez un environnement où Java strictfp a vraiment du sens
Préparer l'environnement de développement Java avec Atom
Préparer l'environnement de développement Java avec VS Code
Apprentissage rapide de Java "Introduction?" Partie 1 Création d'un environnement
Créer un environnement de test E2E avec Selenium (Java)
Préparer un environnement de scraping avec Docker et Java