Operator Remainder na int powoduje java.util.Objects.requireNonNull?

12

Próbuję uzyskać jak największą wydajność z jakiejś metody wewnętrznej.

Kod Java to:

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
}

W moim programie do profilowania zauważyłem, że na procesor przeznaczono 1% java.util.Objects.requireNonNull, ale nawet tego nie nazywam. Podczas sprawdzania kodu bajtowego zobaczyłem to:

 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

Tak więc kompilator generuje to (bezużyteczne?) Sprawdzenie. Pracuję na prymitywach, które i tak nie mogą być null, więc dlaczego kompilator generuje tę linię? Czy to błąd? Czy „normalne” zachowanie?

(Mogę pracować z maską bitową, ale jestem po prostu ciekawy)

[AKTUALIZACJA]

  1. Wydaje się, że operator nie ma z tym nic wspólnego (patrz odpowiedź poniżej)

  2. Używając kompilatora Eclipse (wersja 4.10) otrzymuję ten bardziej rozsądny wynik:

    public getParent (I) Zgłasza wyjątek java / io / IOException 
       L0
        LINENUMBER 77 L0
        ILOAD 1
        ICONST_4
        IREM
        ISTORE 2
       L1
        LINENUMBER 78 L.

To jest bardziej logiczne.

RobAu
źródło
@Lino pewnie, ale to nie jest tak naprawdę istotne dla linii 70 z przyczynamiINVOKESTATIC
RobAu
Z jakiego kompilatora korzystasz? Normalny javactego nie generuje.
apangin
Z jakiego kompilatora korzystasz? Wersja Java, Openjdk / Oracle / itp.? Edycja: Whops, @apangin był szybszy, przepraszam
lugiorgi
1
Jest skompilowany z Intellij 2019.3, z java 11, openjdk version "11.0.6" 2020-01-14na 64-bitowym Ubuntu.
RobAu

Odpowiedzi:

3

Dlaczego nie?

Zarozumiały

class C {
    private final int taxos = 4;

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

połączenie takie jak c.test()gdzie cjest zadeklarowane jako C musi zostać rzucone, kiedy cjest null. Twoja metoda jest równoważna

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

ponieważ pracujesz tylko ze stałymi. Ponieważ testjest niestatyczna, kontrola musi zostać wykonana. Normalnie byłoby to zrobione niejawnie, gdy dostęp do pola lub wywołanie metody niestatycznej zostanie wywołane, ale ty tego nie robisz. Potrzebna jest więc wyraźna kontrola. Jedną z możliwości jest dzwonienie Objects.requireNonNull.

Kod bajtowy

Nie zapominaj, że kod bajtowy jest w zasadzie nieistotny dla wydajności. Zadaniem javacjest stworzenie pewnego kodu bajtowego, którego wykonanie koresponduje z kodem źródłowym. To nie oznaczało, aby zrobić jakieś optymalizacje, jak zoptymalizowany kod jest zazwyczaj dłuższe i trudniejsze do analizy, natomiast kod bajtowy jest faktycznie kod źródłowy dla kompilatora JIT optymalizacji. javacOczekuje się więc, że będzie to proste ....

Wydajność

W moim narzędziu do profilowania zobaczyłem, że na procesor ma 1% java.util.Objects.requireNonNull

Najpierw obwiniłbym profilera. Profilowanie Java jest dość trudne i nigdy nie można oczekiwać doskonałych rezultatów.

Prawdopodobnie powinieneś spróbować ustawić metodę na statyczną. Z pewnością powinieneś przeczytać ten artykuł o zerowych czekach .

maaartinus
źródło
1
Dzięki @maaartinus za wnikliwą odpowiedź. Z pewnością przeczytam twój linkowany artykuł.
RobAu
1
„Ponieważ test jest niestatyczny, kontrola musi zostać wykonana” W rzeczywistości nie ma powodu, aby sprawdzać, czy thisnie jest on niestabilny null. Jak sam powiedziałeś, wywołanie typu c.test()musi zakończyć się niepowodzeniem, gdy cjest, nulli musi zakończyć się niepowodzeniem natychmiast, zamiast wprowadzania metody. Redzie test(), thisnigdy nie może być null(w przeciwnym razie nie byłoby to błąd JVM). Więc nie trzeba sprawdzać. Rzeczywistą poprawką powinna być zmiana pola taxosna static, ponieważ nie ma sensu rezerwowanie pamięci w każdym przypadku na stałą czasową kompilacji. Zatem, czy test()jest staticto nieistotne.
Holger
2

Wygląda na to, że moje pytanie było „nie tak”, ponieważ nie ma nic wspólnego z operatorem, a raczej z samym polem. Nadal nie wiem dlaczego ...

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

Co zamienia się w:

  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
źródło
1
Czy kompilator może się obawiać, że thisodniesienia null? Czy to byłoby możliwe?
atalantus
1
Nie, to nie ma sensu, chyba że kompilator w Integerjakiś sposób skompiluje pole , a to jest wynikiem autoboxowania?
RobAu
1
Nie ALOAD 0dotyczy this? Byłoby więc sensowne (nie do końca), aby kompilator dodał nullcheck
Lino
1
Więc kompilator faktycznie dodaje kontrolę zerową dla this? Świetnie: /
RobAu
1
Spróbuję utworzyć minimalny fragment kodu za pomocą wiersza polecenia, javacaby sprawdzić jutro; a jeśli to również pokazuje takie zachowanie, myślę, że może to być błąd javac?
RobAu
2

Po pierwsze, oto minimalny, powtarzalny przykład tego zachowania:

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

Zachowanie wynika z tego, jak kompilator Java optymalizuje stałe czasu kompilacji .

Zauważ, że w kodzie bajtów foo()nie ma dostępu do odwołania do obiektu, aby uzyskać wartość bar. Jest tak, ponieważ jest to stała czasowa kompilacji, dlatego JVM może po prostu wykonać iconst_5operację, aby zwrócić tę wartość.

Zmieniając barsię w stałą czasową, która nie jest kompilowana (przez usunięcie finalsłowa kluczowego lub nie inicjowanie w deklaracji, ale w konstruktorze) otrzymujesz:

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

gdzie aload_0pcha odniesienie się thisna stosie argumentu aby następnie uzyskać barpola tego obiektu.

Tutaj kompilator jest wystarczająco sprytny, aby zauważyć, że aload_0( thisodniesienie w przypadku funkcji składowych) logicznie nie może być null.

Czy twoja sprawa faktycznie nie zawiera optymalizacji kompilatora?

Zobacz odpowiedź @maaartinus.

atalantus
źródło