L'operatore residuo su int provoca java.util.Objects.requireNonNull?


12

Sto cercando di ottenere il massimo delle prestazioni da un metodo interno.

Il codice Java è:

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
}

Nel mio profiler ho visto che c'è una spesa della CPU dell'1% java.util.Objects.requireNonNull, ma non la chiamo nemmeno. Durante l'ispezione del bytecode, ho visto questo:

 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

Quindi il compilatore genera questo controllo (inutile?). Lavoro su primitive, che non possono essere nullcomunque, quindi perché il compilatore genera questa riga? E 'un errore? O comportamento 'normale'?

(Potrei lavorare con una maschera di bit, ma sono solo curioso)

[AGGIORNARE]

  1. L'operatore sembra non avere nulla a che fare con esso (vedi risposta sotto)

  2. Usando il compilatore eclipse (versione 4.10) ottengo questo risultato più ragionevole:

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

Quindi è più logico.


@Lino certo, ma questo non è rilevante per la linea 70 con causaINVOKESTATIC
RobAu

Quale compilatore usi? Normale javacnon genera questo.
aprile

Quale compilatore usi? Versione Java, Openjdk / Oracle / ecc.? Edit: whops, @apangin era più veloce, scusate
lugiorgi il

1
È compilato da Intellij 2019.3, con Java 11, openjdk version "11.0.6" 2020-01-14su Ubuntu 64 bit.
RobAu,

Risposte:


3

Perchè no?

assumendo

class C {
    private final int taxos = 4;

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

una chiamata come c.test()dove cviene dichiarata come C deve essere lanciata quando cè null. Il tuo metodo è equivalente a

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

mentre lavori solo con costanti. Con testessendo non statica, il controllo deve essere fatto. Normalmente, verrebbe fatto implicitamente quando si accede a un campo o viene chiamato un metodo non statico, ma non lo si fa. Quindi è necessario un controllo esplicito. Una possibilità è quella di chiamare Objects.requireNonNull.

Il bytecode

Non dimenticare che il bytecode è sostanzialmente irrilevante per le prestazioni. Il compito di javacè produrre un bytecode la cui esecuzione corrisponde al codice sorgente. Non ha lo scopo di eseguire alcuna ottimizzazione, in quanto il codice ottimizzato è generalmente più lungo e più difficile da analizzare, mentre il bytecode è in realtà il codice sorgente per il compilatore JIT ottimizzante. Quindi javacdovrebbe essere semplice ...

La prestazione

Nel mio profiler ho visto che c'è una spesa della CPU dell'1% java.util.Objects.requireNonNull

Darei la colpa al profiler prima. Profilare Java è piuttosto difficile e non puoi mai aspettarti risultati perfetti.

Probabilmente dovresti provare a rendere statico il metodo. Dovresti sicuramente leggere questo articolo sui controlli null .


1
Grazie @maaartinus per la tua risposta perspicace. Leggerò sicuramente il tuo articolo collegato.
RobAu

1
"Con il test non statico, il controllo deve essere eseguito" In realtà, non c'è motivo di verificare se thisnon lo è null. Come hai detto tu stesso, una chiamata come c.test()deve fallire quando cè nulle deve fallire immediatamente, invece di entrare nel metodo. Così all'interno test(), thisnon può mai essere null(altrimenti non ci sarebbe un bug JVM). Quindi non c'è bisogno di controllare. La correzione effettiva dovrebbe cambiare il campo taxosin static, poiché non ha senso riservare la memoria in ogni istanza per una costante di compilazione. Quindi, se test()è staticè irrilevante.
Holger,

2

Beh, sembra che la mia domanda fosse "sbagliata" in quanto non ha nulla a che fare con l'operatore, ma piuttosto con il campo stesso. Ancora non so perché ...

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

Che si trasforma in:

  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

1
Il compilatore potrebbe davvero temere che i thisriferimenti null? Questo sarebbe possibile?
Atalanto

1
No, non ha alcun senso, a meno che il compilatore non compili il campo in Integerqualche modo, e questo è il risultato del box automatico?
RobAu,

1
Non fa ALOAD 0riferimento this? Quindi avrebbe senso (non proprio) che il compilatore aggiunga un controllo null
Lino

1
Quindi il compilatore sta effettivamente aggiungendo un controllo null per this? Ottimo: /
RobAu

1
Proverò a creare un codice minimo con la riga di comando javacper verificare domani; e se questo mostra anche questo comportamento, penso che potrebbe essere un javac-bug?
RobAu,

2

Innanzitutto, ecco un esempio riproducibile minimo di questo comportamento:

/**
 * 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;
    }
}

Il comportamento è dovuto al modo in cui il compilatore Java ottimizza le costanti del tempo di compilazione .

Si noti che nel codice byte di foo() nessun oggetto si accede per ottenere il valore di bar. Questo perché è una costante di compilazione e quindi la JVM può semplicemente eseguire l' iconst_5operazione per restituire questo valore.

Quando si cambia barin una costante di tempo di non compilazione (sia rimuovendo ilfinal parola chiave o non inizializzando all'interno della dichiarazione ma all'interno del costruttore) si otterrà:

/**
 * 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;
    }
}

dove aload_0spinge il riferimento di thissullo stack dell'operando a allora ottenere il barcampo di questo oggetto.

Qui il compilatore è abbastanza intelligente da accorgersene aload_0 (il thisriferimento in caso di funzioni membro) non può essere logicamente null.

Ora il tuo caso è effettivamente un'ottimizzazione del compilatore mancante?

Vedi la risposta di @maaartinus.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.