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
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.