Java >> Java tutorial >  >> Tag >> new

Collection.toArray(ny T[0]) eller .toArray(ny T[size])

1. Oversigt

Java-programmeringssproget giver arrays og samlinger til at gruppere objekter sammen. For det meste er en samling understøttet af et array og modelleret med et sæt metoder til at behandle de elementer, den indeholder.

Mens man udvikler software, er det ret almindeligt at bruge begge disse datastrukturer. Derfor har programmører brug for en bromekanisme til at konvertere disse elementer fra en form til en anden. asList metode fra Arrays klasse og Samlingen grænsefladens toArray metode fra denne bro.

I denne øvelse vil vi lave en dybdegående analyse af et interessant argument:som toArray metode at bruge og hvorfor? Vi vil også bruge JMH-assisteret benchmarking til at understøtte disse argumenter.

2. toArray Kaninhul

Før du uden formål påkalder toArray  metode, lad os forstå, hvad der er inde i kassen. Samlingen  interface tilbyder to metoder til at omdanne en samling til en matrix:

Object[] toArray()

<T> T[] toArray(T[] a)

Begge metoder returnerer et array, der indeholder alle elementer i samlingen. For at demonstrere dette, lad os oprette en liste over naturlige tal:

List<Integer> naturalNumbers = IntStream
    .range(1, 10000)
    .boxed()
    .collect(Collectors.toList());

2.1. Collection.toArray()

toArray() metoden tildeler et nyt array i hukommelsen med en længde svarende til samlingens størrelse. Internt, den påberåber sig Arrays.copyOf på det underliggende array, der understøtter samlingen . Derfor har det returnerede array ingen referencer til det og er sikkert at bruge:

Object[] naturalNumbersArray = naturalNumbers.toArray();

Vi kan dog ikke blot støbe resultatet ind i et heltal[]. Hvis du gør det, vil det resultere i en ClassCastException .

2.2. T[] Collection.toArray(T[] a)

I modsætning til den ikke-parametriserede metode, accepterer denne en præ-allokeret matrix som et argument. Derudover kræver brugen af ​​Generics i metodens definition at have samme type for input og det returnerede array. Dette løser også det tidligere observerede problem med iteration over et Objekt[] .

Denne variant fungerer karakteristisk baseret på størrelsen af ​​input-arrayet:

  • Hvis længden af ​​den forudtildelte matrix er mindre end samlingens størrelse, tildeles en ny matrix af den påkrævede længde og samme type:
Integer[] naturalNumbersArray = naturalNumbers.toArray(new Integer[0]);
  • Hvis input-arrayet er stort nok til at indeholde samlingens elementer, returneres det med disse elementer indeni:
Integer[] naturalNumbersArray = naturalNumbers.toArray(new Integer[naturalNumbers.size]);

Lad os nu gå tilbage til det oprindelige spørgsmål om at vælge den hurtigere og bedre præsterende kandidat.

3. Præstationsforsøg

Lad os starte med et simpelt eksperiment, der sammenligner nulstørrelsen (toArray(ny T[0] ) og præ-størrelsen (toArray(ny T[størrelse] ) varianter . Vi bruger den populære ArrayList og AbstractCollection understøttet TreeSet til forsøgene. Vi vil også inkludere samlinger af forskellig størrelse (små, mellemstore og store) for at have et bredt spektrum af prøvedata.

3.1. JMH Benchmark

Lad os derefter sammensætte et JMH (Java Microbenchmark Harness) benchmark til vores forsøg. Vi konfigurerer samlingens størrelse og type parametre for benchmark:

@Param({ "10", "10000", "10000000" })
private int size;

@Param({ "array-list", "tree-set" })
private String type;

Derudover vil vi definere benchmark-metoder for nul-størrelse og præ-størrelse toArray varianter:

@Benchmark
public String[] zero_sized() {
    return collection.toArray(new String[0]);
}

@Benchmark
public String[] pre_sized() {
    return collection.toArray(new String[collection.size()]);
}

3.2. Benchmark-resultater

At køre ovenstående benchmark på en 8 vCPU, 32 GB RAM, Linux x86_64 Virtual Machine med JMH (v1.28) og JDK (1.8.0_292) giver resultaterne vist nedenfor. Score afslører den gennemsnitlige udførelsestid, i nanosekunder pr. operation, for hver af de benchmarkedede metoder.

Jo lavere værdi, jo bedre ydeevne:

Benchmark                   (size)      (type)  Mode  Cnt          Score          Error  Units

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized        10  array-list  avgt   15         24.939 ±        1.202  ns/op
TestBenchmark.pre_sized         10  array-list  avgt   15         38.196 ±        3.767  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized     10000  array-list  avgt   15      15244.367 ±      238.676  ns/op
TestBenchmark.pre_sized      10000  array-list  avgt   15      21263.225 ±      802.684  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized  10000000  array-list  avgt   15   82710389.163 ±  6616266.065  ns/op
TestBenchmark.pre_sized   10000000  array-list  avgt   15  100426920.878 ± 10381964.911  ns/op

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized        10    tree-set  avgt   15         66.802 ±        5.667  ns/op
TestBenchmark.pre_sized         10    tree-set  avgt   15         66.009 ±        4.504  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized     10000    tree-set  avgt   15      85141.622 ±     2323.420  ns/op
TestBenchmark.pre_sized      10000    tree-set  avgt   15      89090.155 ±     4895.966  ns/op
----------------------------------------------------------------------------------------------
TestBenchmark.zero_sized  10000000    tree-set  avgt   15  211896860.317 ± 21019102.769  ns/op
TestBenchmark.pre_sized   10000000    tree-set  avgt   15  212882486.630 ± 20921740.965  ns/op

Efter omhyggelig observation af ovenstående resultater er det helt tydeligt, at nul-størrelsesmetoden kalder vinder det hele, for alle størrelser og samlingstyper i dette forsøg.

For nu er disse tal kun data. For at få en detaljeret forståelse, lad os grave dybt og analysere dem.

3.3. Tildelingssatsen

Hypotetisk kan det antages, at toArray  i nulstørrelse metodekald klarer sig bedre end de foruddefinerede på grund af optimerede hukommelsestildelinger pr. operation . Lad os afklare dette ved at udføre et andet benchmark og kvantificere de gennemsnitlige allokeringshastigheder – hukommelsen i bytes tildelt pr. operation – for de benchmarkerede metoder .

JMH leverer en GC-profiler (-prof gc ), der internt bruger ThreadMXBean#getThreadAllocatedBytes at beregne tildelingssatsen pr. @Benchmark :

Benchmark                                                    (size)      (type)  Mode  Cnt          Score           Error   Units

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized:·gc.alloc.rate.norm                     10  array-list  avgt   15         72.000 ±         0.001    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                      10  array-list  avgt   15         56.000 ±         0.001    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm                  10000  array-list  avgt   15      40032.007 ±         0.001    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                   10000  array-list  avgt   15      40016.010 ±         0.001    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm               10000000  array-list  avgt   15   40000075.796 ±         8.882    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                10000000  array-list  avgt   15   40000062.213 ±         4.739    B/op

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

TestBenchmark.zero_sized:·gc.alloc.rate.norm                     10    tree-set  avgt   15         56.000 ±         0.001    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                      10    tree-set  avgt   15         56.000 ±         0.001    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm                  10000    tree-set  avgt   15      40055.818 ±        16.723    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                   10000    tree-set  avgt   15      41069.423 ±      1644.717    B/op
---------------------------------------------------------------------------------------------------------------------------------
TestBenchmark.zero_sized:·gc.alloc.rate.norm               10000000    tree-set  avgt   15   40000155.947 ±         9.416    B/op
TestBenchmark.pre_sized:·gc.alloc.rate.norm                10000000    tree-set  avgt   15   40000138.987 ±         7.987    B/op

Det er klart, at ovenstående tal beviser, at tildelingsgraden er mere eller mindre den samme for identiske størrelser, uanset samlingstypen eller toArray variant. Derfor nægter det enhver spekulativ antagelse om, at toArray i forhåndsstørrelse og nulstørrelse varianter fungerer anderledes på grund af uregelmæssigheder i deres hukommelsestildelingshastigheder .

3.4. toArray(T[] a) Internal

For yderligere at udrydde årsagen til problemet, lad os dykke ned i ArrayList internt:

if (a.length < size)
    return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
    a[size] = null;
return a;

Grundlæggende er det, afhængigt af længden af ​​det præ-allokerede array, enten en Arrays.copyOf eller den oprindelige System.arraycopy metodekald, der kopierer de underliggende elementer i samlingen til et array.

Yderligere, se på copyOf  metode, er det tydeligt, at der først oprettes en kopi-array af længde svarende til størrelsen af ​​samlingen og derefter efterfulgt af System.arraycopy  påkaldelse:

T[] copy = ((Object)newType == (Object)Object[].class)
    ? (T[]) new Object[newLength]
    : (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
    Math.min(original.length, newLength));

Når både nul-størrelses- og præ-størrelsesmetoden til sidst påkalder den oprindelige System.arraycopy metode, hvordan er metodekaldet i nulstørrelse hurtigere?

Mysteriet ligger i de direkte omkostninger ved CPU-tiden brugt på at udføre nulinitialiseringer for de eksternt forudallokerede arrays, der gør toArray(ny T[size]) metode meget langsommere.

4. Nul initialiseringer

Java-sprogspecifikationen angiver, at nyligt oprettede arrays og objekter skal have standardfeltværdierne og ikke de uregelmæssige rester fra hukommelsen. Derfor skal kørselstiden nulstille det præ-allokerede lager. Benchmarking-eksperimenter har bevist, at array-metodekaldene i nulstørrelse formåede at undgå nulstilling, men det kunne ikke tilfældet i forhåndsstørrelsen.

Lad os overveje et par benchmarks:

@Benchmark
public Foo[] arraycopy_srcLength() {
    Object[] src = this.src;
    Foo[] dst = new Foo[size];
    System.arraycopy(src, 0, dst, 0, src.length);
    return dst;
}

@Benchmark
public Foo[] arraycopy_dstLength() {
    Object[] src = this.src;
    Foo[] dst = new Foo[size];
    System.arraycopy(src, 0, dst, 0, dst.length);
    return dst;
}

Eksperimentelle observationer viser, at System.arraycopy umiddelbart efter arrayallokeringen i arraycopy_srcLength benchmark er i stand til at undgå forudgående nulstilling af dst  matrix . Men arraycopy_dstLength udførelse kunne ikke undgå forudgående nulstilling .

Tilfældigvis er sidstnævnte arraycopy_dstLength case ligner den præ-sized array-metode collection.toArray(new String[collection.size()]) hvor nulstilling ikke kan elimineres, derfor dens langsomhed.

5. Benchmarks på nyere JDK'er

Lad os endelig udføre det originale benchmark på de nyligt udgivne JDK'er, og også konfigurere JVM til at bruge den nyere og meget forbedrede G1 skraldeopsamler:

# VM version: JDK 11.0.2, OpenJDK 64-Bit Server VM, 11.0.2+9
-----------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score    Error  Units
-----------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  199.920 ± 11.309  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  237.342 ± 14.166  ns/op
-----------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  819.306 ± 85.916  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  972.771 ± 69.743  ns/op
###################################################################################

# VM version: JDK 14.0.2, OpenJDK 64-Bit Server VM, 14.0.2+12-46
------------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score    Error   Units
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  158.344 ±   3.862  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  214.340 ±   5.877  ns/op
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  877.289 ± 132.673  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  934.550 ± 148.660  ns/op

####################################################################################

# VM version: JDK 15.0.2, OpenJDK 64-Bit Server VM, 15.0.2+7-27
------------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score     Error  Units
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  147.925 ±   3.968  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  213.525 ±   6.378  ns/op
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  820.853 ± 105.491  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  947.433 ± 123.782  ns/op

####################################################################################

# VM version: JDK 16, OpenJDK 64-Bit Server VM, 16+36-2231
------------------------------------------------------------------------------------
Benchmark                    (size)      (type)  Mode  Cnt    Score     Error  Units
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100  array-list  avgt   15  146.431 ±   2.639  ns/op
ToArrayBenchmark.pre_sized      100  array-list  avgt   15  214.117 ±   3.679  ns/op
------------------------------------------------------------------------------------
ToArrayBenchmark.zero_sized     100    tree-set  avgt   15  818.370 ± 104.643  ns/op
ToArrayBenchmark.pre_sized      100    tree-set  avgt   15  964.072 ± 142.008  ns/op

####################################################################################

Interessant nok er toArray(ny T[0])  metoden har konsekvent været hurtigere end toArray(ny T[størrelse]) . Desuden er dens ydeevne konstant blevet forbedret med hver ny udgivelse af JDK.

5.1. Java 11 Collection.toArray(IntFunction

I Java 11 er Samlingen interface introducerede en ny standard toArray metode, der accepterer en IntFunction generator som et argument (et, der vil generere et nyt array af den ønskede type og den angivne længde).

Denne metode garanterer nyt T[0] array-initialisering ved at påkalde generatorfunktionen med en værdi på nul , og derved sikre, at den hurtigere og bedre ydende toArray(T[]) i nulstørrelse metoden vil altid blive udført.

6. Konklusion

I denne artikel undersøgte vi de forskellige toArray overbelastede metoder i Samlingen  interface. Vi kørte også præstationsforsøg ved at udnytte JMH-mikrobenchmarking-værktøjet på tværs af forskellige JDK'er.

Vi forstod nødvendigheden og virkningen af ​​nulstilling og observerede, hvordan det internt allokerede array eliminerer nulstillingen og dermed vinder præstationsløbet. Endelig kan vi konkludere, at toArray(ny T[0]) varianten er hurtigere end toArray(ny T[størrelse]) og bør derfor altid være den foretrukne mulighed, når vi skal konvertere en samling til et array.

Som altid kan koden, der bruges i denne artikel, findes på GitHub.


Java tag