El operador restante en int causa java.util.Objects.requireNonNull?

12

Estoy tratando de obtener el mayor rendimiento posible de algún método interno.

El código de Java es:

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
}

En mi generador de perfiles vi que hay un 1% de gasto de CPU java.util.Objects.requireNonNull, pero ni siquiera lo llamo. Al inspeccionar el código de bytes, vi esto:

 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

Entonces el compilador genera esta verificación (¿inútil?). Trabajo en primitivas, que no pueden ser de nulltodos modos, entonces, ¿por qué el compilador genera esta línea? ¿Es un error? ¿O comportamiento "normal"?

(Podría trabajar con una máscara de bits, pero tengo curiosidad)

[ACTUALIZAR]

  1. El operador parece no tener nada que ver con eso (ver la respuesta a continuación)

  2. Usando el compilador eclipse (versión 4.10) obtengo este resultado más razonable:

    getParent público (I) lanzo java / io / IOException 
       L0
        LINENUMBER 77 L0
        ILOAD 1
        ICONST_4
        IREM
        ISTORE 2
       L1
        LINENUMBER 78 L

Entonces eso es más lógico.

RobAu
fuente
@Lino seguro, pero eso no es realmente relevante para la línea 70 con las causas deINVOKESTATIC
RobAu
¿Qué compilador usas? Normal javacno genera esto.
Apangin
¿Qué compilador usas? Versión de Java, Openjdk / Oracle / etc. Editar: whops, @apangin fue más rápido, lo siento
lugiorgi
1
Está compilado desde Intellij 2019.3, con java 11, openjdk version "11.0.6" 2020-01-14en ubuntu 64 bit.
RobAu

Respuestas:

3

Por qué no?

Asumiendo

class C {
    private final int taxos = 4;

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

una llamada como c.test()donde cse declara como C debe lanzar cuando ces null. Tu método es equivalente a

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

mientras trabajas solo con constantes. Al testser no estático, la verificación debe hacerse. Normalmente, se haría implícitamente cuando se accede a un campo o se llama a un método no estático, pero no lo hace. Por lo tanto, se necesita una verificación explícita. Una posibilidad es llamar Objects.requireNonNull.

El bytecode

No olvides que el bytecode es básicamente irrelevante para el rendimiento. La tarea de javaces producir algún código de bytes cuya ejecución corresponda con su código fuente. No está destinado a hacer ninguna optimización, ya que el código optimizado suele ser más largo y difícil de analizar, mientras que el código de bytes es en realidad el código fuente para el compilador JIT de optimización. Por javaclo tanto, se espera que sea simple ...

El desempeño

En mi generador de perfiles vi que hay un 1% de gasto de CPU en java.util.Objects.requireNonNull

Primero culparía al perfilador. Perfilar Java es bastante difícil y nunca puedes esperar resultados perfectos.

Probablemente deberías intentar hacer que el método sea estático. Seguramente deberías leer este artículo sobre cheques nulos .

maaartinus
fuente
1
Gracias @maaartinus por tu perspicaz respuesta. Seguramente leeré tu artículo vinculado.
RobAu
1
"Dado que la prueba no es estática, la verificación debe realizarse" En realidad, no hay ninguna razón para probar si thisno es estática null. Como usted mismo dijo, una llamada como c.test()debe fallar cuando ces nully tiene que fallar inmediatamente, en lugar de ingresar al método. Entonces test(), dentro , thisnunca puede ser null(de lo contrario, habría un error JVM). Así que no hay necesidad de verificar. La solución real debería cambiar el campo taxosa static, ya que no tiene sentido reservar memoria en cada instancia para una constante de tiempo de compilación. Entonces, si test()es staticque es irrelevante.
Holger
2

Bueno, parece que mi pregunta fue 'incorrecta' ya que no tiene nada que ver con el operador, sino más bien con el campo en sí. Aún no sé por qué ...

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

Lo que se convierte 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
fuente
1
¿Podría el compilador realmente temer esas thisreferencias null? ¿Sería esto posible?
Atalantus
1
No, eso no tiene ningún sentido, a menos que el compilador compile el campo de Integeralguna manera, ¿y esto es el resultado del autoboxing?
RobAu
1
No hace ALOAD 0referencia this? Por lo tanto, tendría sentido (no realmente) que el compilador agregue un nullcheck
Lino
1
¿Entonces el compilador en realidad está agregando un cheque nulo para this? Genial: /
RobAu
1
Intentaré hacer un código mínimo con la línea de comandos javacpara verificar mañana; y si eso también muestra este comportamiento, creo que podría ser un error de javac?
RobAu
2

En primer lugar, aquí hay un ejemplo mínimo reproducible de este comportamiento:

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

El comportamiento se debe a cómo el compilador de Java optimiza las constantes de tiempo de compilación .

Tenga en cuenta que en el código de byte foo()no se accede a ninguna referencia de objeto para obtener el valor de bar. Esto se debe a que es una constante de tiempo de compilación y, por lo tanto, la JVM simplemente puede ejecutar la iconst_5operación para devolver este valor.

Al cambiar bara una constante de tiempo sin compilación (ya sea eliminando la finalpalabra clave o no inicializando dentro de la declaración pero dentro del constructor) obtendría:

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

donde aload_0empuja la referencia de thisen la pila de operandos para luego obtener el barcampo de este objeto.

Aquí el compilador es lo suficientemente inteligente como para notar que aload_0(la thisreferencia en el caso de funciones miembro) lógicamente no puede serlo null.

¿Ahora su caso es realmente una optimización del compilador que falta?

Ver la respuesta de @maaartinus.

atalantus
fuente