Java >> Java tutorial >  >> Tag >> native

Java hashCode():Tilsidesætte hurtigere den native implementering?

Du har misbrugt JMH, så benchmark-scorerne har ikke meget mening.

  1. Det er normalt ikke nødvendigt at køre noget i en løkke inde i et benchmark. JMH kører selv en benchmark-loop på en måde, der forhindrer JIT-kompileren i at overoptimere den kode, der måles.
  2. Resultater og bivirkninger af den kode, der måles, skal forbruges, enten ved at ringe til Blackhole.consume eller ved at returnere resultatet fra en metode.
  3. Kodens parametre læses typisk fra @State variabler for at undgå konstant foldning og konstant udbredelse.

I dit tilfælde BookWithHash objekter er forbigående:JIT indser, at objekterne ikke undslipper, og eliminerer allokering helt. Da nogle af objektfelterne er konstante, kan JIT endvidere forenkle hashCode beregning ved at bruge konstanter i stedet for at læse objektfelterne.

Tværtimod standard hashCode er afhængig af objektets identitet . Det er derfor tildelingen af ​​Book kan ikke elimineres. Så dit benchmark sammenligner faktisk allokeringen af ​​20.000 objekter (husk på Double objekt) med nogle aritmetiske operationer på de lokale variable og konstanter. Ingen overraskelse, sidstnævnte er meget hurtigere.

En anden ting at tage højde for er, at det første opkald af identitet hashCode er meget langsommere end de efterfølgende kald, fordi hashCoden først skal genereres og lægges ind i objekthovedet. Dette kræver igen et kald til VM runtime. Det andet og de efterfølgende kald af hashCode vil bare få den cachelagrede værdi fra objekthovedet, og dette vil faktisk være meget hurtigere.

Her er et korrigeret benchmark, der sammenligner 4 tilfælde:

  • henter (genererer) en identitets-hashCode for et nyt objekt;
  • få en identitets-hashCode for et eksisterende objekt;
  • beregning af en tilsidesat hashCode for et nyoprettet objekt;
  • beregning af en tilsidesat hashCode for et eksisterende objekt.
@State(Scope.Benchmark)
public class HashCode {

    int id = 123;
    String title = "Jane Eyre";
    String author = "Charlotte Bronte";
    Double price = 14.99;

    Book book = new Book(id, title, author, price);
    BookWithHash bookWithHash = new BookWithHash(id, title, author, price);

    @Benchmark
    public int book() {
        return book.hashCode();
    }

    @Benchmark
    public int bookWithHash() {
        return bookWithHash.hashCode();
    }

    @Benchmark
    public int newBook() {
        return (book = new Book(id, title, author, price)).hashCode();
    }

    @Benchmark
    public int newBookWithHash() {
        return (bookWithHash = new BookWithHash(id, title, author, price)).hashCode();
    }
}
Benchmark                 Mode  Cnt   Score   Error  Units
HashCode.book             avgt    5   2,907 ± 0,032  ns/op
HashCode.bookWithHash     avgt    5   5,052 ± 0,119  ns/op
HashCode.newBook          avgt    5  74,280 ± 5,384  ns/op
HashCode.newBookWithHash  avgt    5  14,401 ± 0,041  ns/op

Resultaterne viser, at det er betydeligt hurtigere at få en identitetshashCode for et eksisterende objekt end at beregne hashCode over objektfelterne (2,9 vs. 5 ns). Generering af en ny identitets-hashCode er imidlertid en meget langsom operation, selv sammenlignet med en objektallokering.


Ydeevneforskellen skyldes, at du opretter et nyt objekt for hver hashCode() påkaldelse i benchmark, og standard hashCode() implementering cacher sin værdi i objektheaderen, mens den brugerdefinerede uvidende ikke gør det. Det tager meget tid at skrive til objekthovedet, da det involverer et indbygget kald.

Gentagne påkaldelser af standard hashCode() implementeringen fungerer lidt bedre end den brugerdefinerede.

Hvis du indstiller -XX:-UseBiasedLocking , vil du se, at ydelsesforskellen falder. Da forudindtaget låseinformation også er gemt i objektoverskrifter, og deaktivering af det påvirker objektlayoutet, er dette et yderligere bevis.


Java tag