O operador restante no int causa java.util.Objects.requireNonNull?

12

Estou tentando obter o máximo desempenho possível de algum método interno.

O código 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
}

No meu profiler, vi 1% de gasto de CPU java.util.Objects.requireNonNull, mas nem chamo isso. Ao inspecionar o bytecode, vi o seguinte:

 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

Portanto, o compilador gera essa verificação (inútil?). Eu trabalho com primitivas, que não podem ser nullassim, por que o compilador gera essa linha? Isso é um bug? Ou comportamento 'normal'?

(Eu posso trabalhar com uma máscara de bit, mas estou curioso)

[ATUALIZAR]

  1. O operador parece não ter nada a ver com isso (veja a resposta abaixo)

  2. Usando o compilador eclipse (versão 4.10), obtenho esse resultado mais razoável:

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

Então isso é mais lógico.

RobAu
fonte
@Lino claro, mas isso não é realmente relevante para a linha 70 com causasINVOKESTATIC
RobAu
Qual compilador você usa? Normal javacnão gera isso.
apangin 03/02
Qual compilador você usa? Versão Java, Openjdk / Oracle / etc.? Edit: whops, @apangin foi mais rápido, desculpe
lugiorgi
11
É compilado a partir do Intellij 2019.3, com java 11, openjdk version "11.0.6" 2020-01-14no ubuntu 64 bits.
RobAu

Respostas:

3

Por que não?

Assumindo

class C {
    private final int taxos = 4;

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

uma chamada como c.test()onde cé declarada como C deve ser lançada quando cé null. Seu método é equivalente a

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

como você trabalha apenas com constantes. Com testsendo não-estático, o cheque deve ser feito. Normalmente, isso seria feito implicitamente quando um campo é acessado ou um método não estático é chamado, mas você não faz isso. Portanto, é necessária uma verificação explícita. Uma possibilidade é ligar Objects.requireNonNull.

O bytecode

Não esqueça que o bytecode é basicamente irrelevante para o desempenho. A tarefa de javacé produzir algum bytecode cuja execução corresponda ao seu código fonte. Isso não deveria fazer qualquer otimizações, como código otimizado é geralmente mais longo e mais difícil de analisar, enquanto o bytecode é , na verdade, o código-fonte para o compilador otimizado JIT. Portanto, javacespera-se mantê-lo simples ....

O desempenho

No meu profiler, vi 1% de gasto de CPU em java.util.Objects.requireNonNull

Eu culparia o criador de perfil primeiro. A criação de perfil do Java é bastante difícil e você nunca pode esperar resultados perfeitos.

Você provavelmente deve tentar tornar o método estático. Você certamente deveria ler este artigo sobre verificações nulas .

maaartinus
fonte
11
Obrigado @maaartinus pela sua resposta perspicaz. Certamente vou ler o seu artigo vinculado.
RobAu
11
“Com o teste sendo não estático, a verificação deve ser feita” Na verdade, não há motivo para testar se thisé não null. Como você mesmo disse, uma chamada como c.test()deve falhar quando cé nulle tem que falhar imediatamente, em vez de digitar o método. Portanto test(), por dentro , thisnunca pode ser null(caso contrário, haveria um bug da JVM). Portanto, não há necessidade de verificar. A correção real deve estar alterando o campo taxospara static, pois não há sentido em reservar memória em todas as instâncias para uma constante em tempo de compilação. Então, se test()é static, é irrelevante.
Holger
2

Bem, parece que minha pergunta estava "errada", pois não tem nada a ver com o operador, mas com o próprio campo. Ainda não sei por que ..

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

Que se transforma em:

  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
fonte
11
Poderia o compilador realmente ter medo que as thisreferências null? Isso seria possível?
atalantus 03/02
11
Não, isso não faz sentido, a menos que o compilador compile o campo de Integeralguma forma, e este é o resultado da caixa automática?
RobAu 03/02
11
Não faz ALOAD 0referência this? Portanto, faria sentido (não realmente) que o compilador adicione uma verificação nula
Lino
11
Então, o compilador está realmente adicionando uma verificação nula this? Ótimo: /
RobAu 03/02
11
Vou tentar criar um pedaço mínimo de código com a linha de comando javacpara verificar amanhã; e se isso também mostrar esse comportamento, acho que pode ser um bug de javac?
RobAu
2

Em primeiro lugar, aqui está um exemplo reprodutível mínimo desse 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;
    }
}

O comportamento é devido à forma como o compilador Java otimiza constantes em tempo de compilação .

Observe que no código de bytes de foo()nenhuma referência a objeto é acessada para obter o valor de bar. Isso ocorre porque é uma constante em tempo de compilação e, portanto, a JVM pode simplesmente executar oiconst_5 operação para retornar esse valor.

Ao mudar barpara uma constante de tempo não compilada (removendo a finalpalavra - chave ou não inicializando dentro da declaração, mas dentro do construtor), você obteria:

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

onde aload_0empurra a referência de thisna pilha operando para, em seguida, obter o barcampo do objeto.

Aqui, o compilador é inteligente o suficiente para notar que aload_0(a thisreferência no caso de funções-membro) logicamente não pode sernull .

Agora, o seu caso é realmente uma otimização de compilador ausente?

Consulte a resposta @maaartinus.

atalantus
fonte