L'opérateur restant sur int provoque java.util.Objects.requireNonNull?

12

J'essaie d'obtenir autant de performances que possible à partir d'une méthode interne.

Le code Java est:

List<DirectoryTaxonomyWriter> writers = Lists.newArrayList();
private final int taxos = 4;

[...]

@Override
public int getParent(final int globalOrdinal) throws IOException {
    final int bin = globalOrdinal % this.taxos;
    final int ordinalInBin = globalOrdinal / this.taxos;
    return this.writers.get(bin).getParent(ordinalInBin) * this.taxos + bin; //global parent
}

Dans mon profileur, j'ai vu qu'il y avait 1% de dépenses CPU java.util.Objects.requireNonNull, mais je n'appelle même pas cela. Lors de l'inspection du bytecode, j'ai vu ceci:

 public getParent(I)I throws java/io/IOException 
   L0
    LINENUMBER 70 L0
    ILOAD 1
    ALOAD 0
    INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    BIPUSH 8
    IREM
    ISTORE 2

Le compilateur génère donc cette vérification (inutile?). Je travaille sur des primitives, ce qui ne peut pas être de nulltoute façon, alors pourquoi le compilateur génère-t-il cette ligne? Est-ce un bug? Ou un comportement «normal»?

(Je pourrais travailler avec un bitmask, mais je suis juste curieux)

[MISE À JOUR]

  1. L'opérateur semble n'avoir rien à voir avec cela (voir la réponse ci-dessous)

  2. En utilisant le compilateur eclipse (version 4.10) j'obtiens ce résultat plus raisonnable:

    public getParent (I) I lève java / io / IOException 
       L0
        LINENUMBER 77 L0
        ILOAD 1
        ICONST_4
        IREM
        ISTORE 2
       L1
        LINENUMBER 78 L

C'est donc plus logique.

RobAu
la source
@Lino bien sûr, mais ce n'est pas vraiment pertinent pour la ligne 70 avec les causesINVOKESTATIC
RobAu
Quel compilateur utilisez-vous? Normal javacne génère pas cela.
Apangin
Quel compilateur utilisez-vous? Version Java, Openjdk / Oracle / etc.? Edit: whops, @apangin était plus rapide, désolé
lugiorgi
1
Il est compilé à partir d'Intellij 2019.3, avec java 11, openjdk version "11.0.6" 2020-01-14sur ubuntu 64 bits.
RobAu

Réponses:

3

Pourquoi pas?

En supposant

class C {
    private final int taxos = 4;

    public int test() {
        final int a = 7;
        final int b = this.taxos;
        return a % b;
    }
}

un appel comme c.test()cest déclaré comme C doit lancer quand cest null. Votre méthode équivaut à

    public int test() {
        return 3; // `7 % 4`
    }

car vous ne travaillez qu'avec des constantes. Étant testnon statique, la vérification doit être effectuée. Normalement, cela se ferait implicitement lorsqu'un champ est accédé ou qu'une méthode non statique est appelée, mais vous ne le faites pas. Une vérification explicite est donc nécessaire. Une possibilité est d'appeler Objects.requireNonNull.

Le bytecode

N'oubliez pas que le bytecode n'est fondamentalement pas pertinent pour les performances. La tâche de javacest de produire un bytecode dont l'exécution correspond à votre code source. Il n'est pas destiné à effectuer des optimisations, car le code optimisé est généralement plus long et plus difficile à analyser, tandis que le bytecode est en fait le code source du compilateur JIT d'optimisation. Doncjavac s'attend donc à ce que cela reste simple ....

La performance

Dans mon profileur, j'ai vu qu'il y avait 1% de dépenses CPU java.util.Objects.requireNonNull

Je blâmerais d'abord le profileur. Le profilage de Java est assez difficile et vous ne pouvez jamais vous attendre à des résultats parfaits.

Vous devriez probablement essayer de rendre la méthode statique. Vous devriez sûrement lire cet article sur les contrôles nuls .

maaartinus
la source
1
Merci @maaartinus pour votre réponse perspicace. Je vais sûrement lire votre article lié.
RobAu
1
«Le test étant non statique, le contrôle doit être effectué.» En fait, il n'y a aucune raison de vérifier s'il ne l' thisest pas null. Comme vous l'avez dit vous-même, un appel comme celui-ci c.test()doit échouer quand il l' cest nullet il doit échouer immédiatement, au lieu d'entrer dans la méthode. Donc à l'intérieur test(), ça thisne peut jamais être null(sinon il y aurait un bug JVM). Donc pas besoin de vérifier. Le correctif réel devrait changer le champ taxosen static, car il est inutile de réserver de la mémoire dans chaque instance pour une constante de temps de compilation. Alors, que ce soit test()est staticest hors de propos.
Holger
2

Eh bien, il semble que ma question était «fausse» car elle n'a rien à voir avec l'opérateur, mais plutôt avec le champ lui-même. Je ne sais toujours pas pourquoi ..

   public int test() {
        final int a = 7;
        final int b = this.taxos;
        return a % b;
    }

Qui se transforme en:

  public test()I
   L0
    LINENUMBER 51 L0
    BIPUSH 7
    ISTORE 1
   L1
    LINENUMBER 52 L1
    ALOAD 0
    INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    ICONST_4
    ISTORE 2
   L2
    LINENUMBER 53 L2
    BIPUSH 7
    ILOAD 2
    IREM
    IRETURN
RobAu
la source
1
Le compilateur pourrait-il réellement avoir peur que les thisréférences null? Serait-ce possible?
atalantus
1
Non, cela n'a aucun sens, à moins que le compilateur compile le champ en Integerquelque sorte, et c'est le résultat de l'autoboxing?
RobAu
1
Ne fait pas ALOAD 0référence this? Il serait donc logique (pas vraiment) que le compilateur ajoute un nullcheck
Lino
1
Donc, le compilateur ajoute en fait un contrôle nul pour this? Super: /
RobAu
1
J'essaierai de faire un minimum de code avec la ligne javacde commande pour vérifier demain; et si cela montre également ce comportement, je pense que ce pourrait être un bug javac?
RobAu
2

Tout d'abord, voici un exemple reproductible minimal de ce comportement:

/**
 * OS:              Windows 10 64x
 * javac version:   13.0.1
 */
public class Test {
    private final int bar = 5;

    /**
     * public int foo();
     *   Code:
     *     0: iconst_5
     *     1: ireturn
     */
    public int foo() {
        return bar;
    }

    /**
     * public int foo2();
     *   Code:
     *     0: aload_0
     *     1: invokestatic  #13     // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
     *     4: pop
     *     5: iconst_5
     *     6: ireturn
     */
    public int foo2() {
        return this.bar;
    }
}

Le comportement est dû à la façon dont le compilateur Java optimise les constantes au moment de la compilation .

Notez que dans le code d'octet foo()aucune référence d'objet n'est accessible pour obtenir la valeur de bar. C'est parce que c'est une constante de temps de compilation et donc la JVM peut simplement exécuter leiconst_5 opération pour renvoyer cette valeur.

En changeant baren constante de temps non compilée (soit en supprimant le finalmot - clé, soit en ne l'initialisant pas dans la déclaration mais à l'intérieur du constructeur), vous obtenez:

/**
 * OS:              Windows 10 64x
 * javac version:   13.0.1
 */
public class Test2 {
    private int bar = 5;

    /**
     * public int foo();
     *   Code:
     *     0: aload_0
     *     1: getfield      #7
     *     4: ireturn
     */
    public int foo() {
        return bar;
    }

    /**
     * public int foo2();
     *   Code:
     *     0: aload_0
     *     1: getfield      #7
     *     4: ireturn
     */
    public int foo2() {
        return this.bar;
    }
}

aload_0 pousse la référence de thissur la pile d'opérandes pour ensuite obtenir le barchamp de cet objet.

Ici, le compilateur est assez intelligent pour remarquer que aload_0(la thisréférence en cas de fonctions membres) ne peut logiquement pas êtrenull .

Votre cas est-il en fait une optimisation de compilateur manquante?

Voir la réponse @maaartinus.

atalantus
la source