In diesem Beitrag wird die Diskussion über Leistungsverbesserungen fortgesetzt, die sich ohne die verschiedenen Buts ergeben könnten. Der vorherige Teil über StringBuilder
ist hier .
Hier sehen wir uns einige „Verbesserungen“ an, die aufgrund mangelnden Verständnisses der Feinheiten der Sprachspezifikation, nicht offensichtlicher Eckfälle und anderer Gründe abgelehnt wurden. Lass uns gehen!
Wenn nichts Ärger bedeutet
Ich denke, jeder von uns hat mit den Methoden Collections.emptySet()
/ Collections.emptyList()
. Dies sind sehr nützliche Methoden, mit denen Sie eine leere unveränderliche Sammlung zurückgeben können, ohne ein neues Objekt zu erstellen. Wenn wir uns die EmptyList
Klasse EmptyList
wir EmptyList
:
private static class EmptyList<E> { public Iterator<E> iterator() { return emptyIterator(); } public Object[] toArray() { return new Object[0]; } }
Starkes Verbesserungspotential sehen? Die EmptyList.iterator()
-Methode gibt einen leeren Iterator aus der Gegenwart zurück. Warum machen Sie nicht dieselbe Finte mit Ihren Ohren für das Array, das von der toArray()
-Methode zurückgegeben wird?
Mit anderen Worten, die Methode sollte immer ein neues Array zurückgeben.
Sie werden sagen: "Er ist unveränderlich! Was kann schief gehen !?"
Nur erfahrene Experten können diese Frage beantworten:
- Wer ist verantwortlich?
- Verantwortliche Experten Paul Sandoz und Tagir Valeev
Die Antwort von Expertenhttp://mail.openjdk.java.net/pipermail/core-libs-dev/2017-September/049171.html
Beachten Sie auch, dass dies das sichtbare Verhalten ändert. Zum Beispiel. Möglicherweise synchronisiert jemand das vom toArray-Aufruf zurückgegebene Array-Objekt, sodass diese Änderung möglicherweise zu einer unerwünschten Freigabe der Sperren führt .
Einmal schlug ich eine ähnliche Verbesserung vor: EMPTY_LIST von Arrays.asList () zurückzugeben, wenn das angegebene Array die Länge Null hat. Es wurde aus dem gleichen Grund abgelehnt [1].
http://mail.openjdk.java.net/pipermail/core-libs-dev/2015-September/035197.html
Übrigens ist es dann wahrscheinlich sinnvoll, dass Arrays.asList die Array-Länge wie folgt überprüft:
public static <T> List<T> asList(T... a) { if(a.length == 0) return Collections.emptyList(); return new ArrayList<>(a); }
Klingt vernünftig, oder? Warum eine neue Liste für ein leeres Array erstellen, wenn Sie eine fertige kostenlos herunterladen können?
Es gibt einen Grund, dies nicht zu tun. Derzeit gibt Arrays.asList keine Einschränkungen für die Identität der zurückgegebenen Liste an. Das Hinzufügen der Mikrooptimierung wird dies ändern. Es ist ein Randfall und ein fragwürdiger Anwendungsfall, aber wenn man bedenkt, dass ich die Dinge konservativ so lassen würde, wie sie sind.
Diese Aussage wird Sie wahrscheinlich verwirren:
Zum Beispiel. Möglicherweise synchronisiert jemand das vom toArray-Aufruf zurückgegebene Array-Objekt, sodass diese Änderung möglicherweise zu einer unerwünschten Freigabe der Sperren führt.
Sie werden sagen: "Wer bei klarem Verstand wird auf dem Array synchronisiert (!), Das aus der Sammlung zurückgegeben wurde (!!!) !?"
Es klingt nicht sehr glaubwürdig, aber die Sprache bietet eine solche Möglichkeit, was bedeutet, dass die Möglichkeit besteht, dass ein bestimmter Benutzer dies tut (oder es sogar bereits getan hat). Dann wird die vorgeschlagene Änderung im besten Fall das Verhalten des Codes ändern und im schlimmsten Fall zu einer Unterbrechung der Synchronisation führen (später gehen, abfangen). Das Risiko ist so ungerechtfertigt und der erwartete Gewinn so unbedeutend, dass es besser ist, alles so zu lassen, wie es ist.
Im Allgemeinen war die Fähigkeit zur Synchronisierung auf jedem Objekt, kmk, der Fehler der Sprachentwickler. Erstens enthält der Header jedes Objekts eine Struktur, die für die Synchronisation verantwortlich ist, und zweitens befinden wir uns in der oben beschriebenen Situation, in der ein scheinbar unveränderliches Objekt nicht mehrmals zurückgegeben werden kann, da es darauf synchronisiert werden kann.
Die Moral dieser Fabel lautet: Spezifikation und Abwärtskompatibilität sind heilige Kühe von Java. Versuchen Sie nicht einmal, in sie einzudringen: Der Wachmann schießt ohne Vorwarnung.
Versuchen, versuchen ...
Es gibt mehrere Array-basierte Klassen im JDK gleichzeitig, und alle implementieren die Methoden List.indexOf()
und List.lastIndexOf()
:
- java.util.ArrayList
- java.util.Arrays $ ArrayList
- java.util.Vector
- java.util.concurrent.CopyOnWriteArrayList
Der Code dieser Methoden in diesen Klassen wird fast eins zu eins wiederholt. Viele Anwendungen und Frameworks bieten auch Lösungen für dasselbe Problem an:
Als Ergebnis haben wir streunenden Code, der kompiliert werden muss (manchmal mehrmals), der im ReserverCodeCache stattfindet, der getestet werden muss und der einfach fast ohne Änderungen von Klasse zu Klasse wandert.
Entwickler wiederum schreiben sehr gerne so etwas
int i = Arrays.asList(array).indexOf(obj);
Ich möchte verallgemeinerte Dienstprogrammmethoden in das JDK einführen und sie wie vorgeschlagen überall verwenden. Der Patch ist so einfach wie zwei Cent:
1) Implementierungen von List.indexOf()
und List.lastIndexOf()
nach java.util.Arrays
verschoben
2) Stattdessen werden Arrays.indexOf()
und Arrays.lastIndexOf()
aufgerufen
Es scheint, dass was schief gehen könnte? Der Gewinn aus diesem Ansatz ist [scheinbar] offensichtlich. Aber der Artikel handelt von Fehlern. Überlegen Sie also, was schief gehen könnte.
- Wer ist verantwortlich?
- Verantwortliche Experten Martin Buchholz und Paul Sandoz
IMHO, ein wenig angespannt, aber trotzdemMartin Buchholz:
Sergey, ich pflege sozusagen alle diese Auflistungsklassen, und gelegentlich wollte ich auch indexOf-Methoden in Array.java haben. Aber:
Arrays werden im Allgemeinen nicht empfohlen. Alle neuen statischen Methoden für Arrays (oder, wo ich sie eigentlich möchte, für das Array-Objekt selbst! Erfordert eine Änderung der Java-Sprache!) Stoßen auf Widerstand.
Wir haben es bereut, Nullen in Sammlungen unterstützt zu haben, daher unterstützen neuere Sammlungsklassen wie ArrayDeque sie nicht.
Eine andere Variante, die Benutzer möglicherweise wünschen, ist die Art des zu verwendenden Gleichheitsvergleichs.
Wir haben es bereut, ArrayList mit einem auf Null basierenden Startindex zu haben - es wäre besser gewesen, das kreisförmige Array-Verhalten von ArrayDeque vom ersten Tag an zu haben.
Der Code zum Durchsuchen von Array-Slices ist sehr klein, sodass Sie nicht viel sparen. Es ist einfach, einen Fehler nach dem anderen zu machen, aber das gilt auch für eine Arrays-API.
Paul Sandoz:
Ich würde nicht so weit gehen zu sagen, dass Arrays entmutigt sind, ich würde es positiv als "vorsichtig verwenden" bezeichnen, da sie stachelig sind, z. B. immer veränderlich. Sie könnten sicherlich verbessert werden. Ich würde mich sehr freuen, wenn Arrays ein gemeinsames Array implementieren 'ish Schnittstelle, können wir möglicherweise einige Fortschritte nach Werttypen Sediment machen.
Alle neuen Ergänzungen zu Arrays würden jedoch zumindest von mir auf Widerstand stoßen :-) Es werden nie nur ein oder zwei Methoden hinzugefügt, viele andere möchten ebenfalls mitfahren (alle Grundelemente plus Reichweitenvarianten). Daher muss jede neue Funktion ausreichend nützlich sein, und in diesem Fall denke ich nicht, dass die Vorteile stark genug sind (z. B. ein möglicher Reduktionscode-Cache-Druck).
Paul.
Korrespondenz: http://mail.openjdk.java.net/pipermail/core-libs-dev/2018-March/051968.html
Die Moral dieser Fabel lautet: Ihr genialer Patch kann zur Überprüfung geschlachtet werden, einfach weil sie keinen besonderen Wert darin sehen. Ja, es gibt doppelten Code, aber er stört niemanden. Lassen Sie ihn also leben.
Verbesserungen für ArrayList? Ich habe sie
Moped Der Patch gehört nicht mir. Ich werde ihn nur veröffentlichen, damit Sie darüber nachdenken können. Der Vorschlag selbst wurde hier geäußert und sieht sehr attraktiv aus. Überzeugen Sie sich selbst:
Mit bloßem Auge ist der Vorschlag sehr, sehr logisch. Sie können die Leistung anhand eines einfachen Benchmarks messen:
@State(Scope.Benchmark) public class ArrayListBenchmark { @State(Scope.Benchmark) public static class Data { @Param({"10", "100", "1000", "10000"}) public int size; ArrayList<Integer> arrayRandom = new ArrayList<Integer>(size); @Setup(Level.Invocation) public void initArrayList() { Random rand = new Random(); rand.setSeed(System.currentTimeMillis());
Zusammenfassung:
Benchmark (size) Mode Cnt Score Error Units construct_new_array_list 10 thrpt 25 388.212 ± 23.110 ops/s construct_new_array_list 100 thrpt 25 90.208 ± 7.995 ops/s construct_new_array_list 1000 thrpt 25 23.289 ± 1.687 ops/s construct_new_array_list 10000 thrpt 25 7.659 ± 0.560 ops/s construct_new_array_list 10 thrpt 25 562.678 ± 37.370 ops/s construct_new_array_list 100 thrpt 25 119.791 ± 13.232 ops/s construct_new_array_list 1000 thrpt 25 33.811 ± 3.812 ops/s construct_new_array_list 10000 thrpt 25 10.889 ± 0.564 ops/s
Nicht schlecht für eine so einfache Änderung. Die Hauptsache ist, dass es keinen Haken zu geben scheint. Erstellen Sie ehrlich ein Array, kopieren Sie die Daten ehrlich und vergessen Sie nicht die Größe. Jetzt müssen sie den Patch definitiv akzeptieren!
Aber da war esMartin Buchholz:
Dies ist keine Frage, dass wir den Fall von ArrayList -> ArrayList optimieren können, aber was ist mit all den anderen Collection-Implementierungen? ArrayDeque und CopyOnWriteArrayList fallen mir ein.
ArrayList ist eine beliebte Klasse zum Erstellen von Kopien von Sammlungen. Wo hörst du auf?
Eine pathologische Unterklasse von ArrayList könnte entscheiden, keine Elemente im Hintergrundarray zu speichern, was zu einem Bruch führt.
Die gesegnete Lösung für das Problem der Listenkopie ist wahrscheinlich List.copyOf https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html#copyOf(java .util.Collection ), die möglicherweise die von Ihnen erhoffte Optimierung durchführen.
Alan Bateman
ArrayList ist nicht endgültig, daher ist es möglich, dass jemand es erweitert hat, um etwas anderes als elementData zu verwenden. Es ist möglicherweise sicherer, die Klassenidentität als die Instanz von zu verwenden.
Nichts verbietet mir, mich von ArrayList
zu ArrayList
und Daten in einer verknüpften Liste zu speichern. Dann wird eine c instanceof ArrayList
die Wahrheit zurückgeben, wir werden zum c instanceof ArrayList
gelangen und sicher fallen.
Die Moral dieser Fabel lautet: Eine mögliche Verhaltensänderung kann die Ursache für das Scheitern sein. Mit anderen Worten, man muss die Wahrscheinlichkeit selbst der absurdesten Veränderung im Auge behalten, wenn dies durch die Sprache erlaubt ist. Und ja, es hätte ArrayList
, wenn ArrayList
Anfang an für final
erklärt hätte.
Spezifikation erneut
Beim Debuggen meiner Anwendung bin ich versehentlich in den Bauch von Spring geraten und habe den folgenden Code gefunden :
Glücklicherweise habe ich durch das java.lang.reflect.Constructor.getParameterTypes()
von java.lang.reflect.Constructor.getParameterTypes()
den Code etwas tiefer gescrollt und einen schönen gefunden:
@Override public Class<?>[] getParameterTypes() { return parameterTypes.clone(); } public int getParameterCount() { return parameterTypes.length; }
Siehst du, ja? Wenn wir die Anzahl der Konstruktor- / Methodenargumente herausfinden müssen, rufen Sie einfach java.lang.reflect.Method.getParameterCount()
und verzichten Sie auf das Kopieren des Arrays. Überprüfen Sie, ob das Spiel im einfachsten Fall, in dem die Methode keine Parameter hat, die Kerze wert ist:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MethodToStringBenchmark { private Method method; @Setup public void setup() throws Exception { method = getClass().getMethod("toString"); } @Benchmark public int getParameterCount() { return method.getParameterCount(); } @Benchmark public int getParameterTypes() { return method.getParameterTypes().length; } }
Auf meinem Computer und mit JDK 11 sieht es so aus:
Benchmark Mode Cnt Score Error Units getParameterCount avgt 25 2,528 ± 0,085 ns/op getParameterCount:·gc.alloc.rate avgt 25 ≈ 10⁻⁴ MB/sec getParameterCount:·gc.alloc.rate.norm avgt 25 ≈ 10⁻⁷ B/op getParameterCount:·gc.count avgt 25 ≈ 0 counts getParameterTypes avgt 25 7,299 ± 0,410 ns/op getParameterTypes:·gc.alloc.rate avgt 25 1999,454 ± 89,929 MB/sec getParameterTypes:·gc.alloc.rate.norm avgt 25 16,000 ± 0,001 B/op getParameterTypes:·gc.churn.G1_Eden_Space avgt 25 2003,360 ± 91,537 MB/sec getParameterTypes:·gc.churn.G1_Eden_Space.norm avgt 25 16,030 ± 0,045 B/op getParameterTypes:·gc.churn.G1_Old_Gen avgt 25 0,004 ± 0,001 MB/sec getParameterTypes:·gc.churn.G1_Old_Gen.norm avgt 25 ≈ 10⁻⁵ B/op getParameterTypes:·gc.count avgt 25 2380,000 counts getParameterTypes:·gc.time avgt 25 1325,000 ms
Was können wir dagegen tun? Wir können nach der Verwendung der Antipattern- Method.getParameterTypes().length
im JDK (zumindest in java.base
) und dort ersetzen, wo es Sinn macht:
java.lang.invoke.MethodHandleProxies

java.util.concurrent.ForkJoinTask

java.lang.reflect.Executable

sun.reflect.annotation.AnnotationType

Der Patch wurde zusammen mit einem Anschreiben verschickt.
Plötzlich stellte sich heraus, dass es seit einigen Jahren eine ähnliche Aufgabe gibt und sogar Änderungen darauf vorbereitet wurden. In den Kommentaren wurde eine recht ordentliche Leistungssteigerung für solche einfachen Änderungen festgestellt. Allerdings sind sowohl sie als auch mein Patch bisher gereinigt und liegen regungslos da. Warum? Wahrscheinlich, weil die Entwickler zu beschäftigt mit notwendigeren Dingen sind und sie es dummerweise nicht in die Hände bekommen.
Die Moral dieser Fabel lautet: Ihre genialen Veränderungen können einfach aufgrund eines Mangels an Arbeitern einfrieren.
Aber das ist nicht das Ende! Während der Diskussion über die Rationalität des beschriebenen Ersatzes in anderen Projekten haben erfahrene Kameraden einen Gegenvorschlag unterbreitet: Vielleicht sollten Sie Method.getParameterTypes().length -> Method.getParameterCount()
mit Ihren Händen Method.getParameterTypes().length -> Method.getParameterCount()
, sondern dem Compiler anvertrauen? Ist das möglich und wird es "legal" sein?
Versuchen wir es mit dem Test zu überprüfen:
@Test void arrayClone() { final Object[] objects = new Object[3]; objects[0] = "azaza"; objects[1] = 365; objects[2] = 9876L; final Object[] clone = objects.clone(); assertEquals(objects.length, clone.length); assertSame(objects[0], clone[0]); assertSame(objects[1], clone[1]); assertSame(objects[2], clone[2]); }
was passiert und was zeigt, dass wenn das geklonte Array den Bereich nicht verlässt, es gelöscht werden kann, weil der Zugriff auf ein Element aus seinen Zellen oder dem length
vom Original erhalten werden kann.
Kann das JDK das tun? Wir prüfen:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class ArrayAllocationEliminationBenchmark { private int length = 10; @Benchmark public int baseline() { return new int[length].length; } @Benchmark public int baselineClone() { return new int[length].clone().length; } }
Ausgabe für JDK 13:
Benchmark Mode Cnt Score Error Units baseline avgt 50 6,135 ± 0,140 ns/op baseline:·gc.alloc.rate.norm avgt 50 56,000 ± 0,001 B/op clone avgt 50 18,359 ± 0,619 ns/op clone:·gc.alloc.rate.norm avgt 50 112,000 ± 0,001 B/op
Es stellt sich heraus, dass openjdk im Gegensatz zum Gral nicht weiß, wie man new int[length]
wirft, hehe:
Benchmark Mode Cnt Score Error Units baseline avgt 25 2,470 ± 0,156 ns/op baseline:·gc.alloc.rate.norm avgt 25 0,005 ± 0,008 B/op lone avgt 25 13,086 ± 1,059 ns/op lone:·gc.alloc.rate.norm avgt 25 112,113 ± 0,115 B/op
Es stellt sich heraus, dass Sie den openjdk-Optimierungscompiler ein wenig optimieren können, damit er das kann, was der Gral kann. Da nicht nur jeder eine positive Anzeige im VM-Code schalten und etwas Sinnvolles einreichen kann, beschränkte ich mich auf einen Brief, in dem ich meine Beobachtungen darlegte.
Es stellte sich heraus, und es gibt mehrere Feinheiten. Vladimir Ivanov gibt an, dass:
In Anbetracht der Tatsache, dass es keine Möglichkeit gibt, Java-Arrays zu vergrößern oder zu verkleinern,
Die Transformation "cloned_array.length => original_array.length" ist korrekt
unabhängig davon, ob die geklonte Variante entkommt oder nicht.
Darüber hinaus ist die Transformation bereits da:
http://hg.openjdk.java.net/jdk/jdk/file/tip/src/hotspot/share/opto/memnode.cpp#l2388
Ich habe mir die von Ihnen erwähnten Benchmarks nicht angesehen, aber es sieht so aus
Der Zugriff auf cloned_array.length ist nicht der Grund, warum das geklonte Array immer noch vorhanden ist
dort.
In Bezug auf Ihre anderen Ideen können Sie Zugriffe von der geklonten Instanz auf umleiten
Original ist problematisch (im Allgemeinen), da der Compiler beweisen muss
Es gab keine Änderungen in beiden Versionen und indizierte Zugriffe machen es gleichmäßig
schwerer. Und Sicherheitspunkte verursachen ebenfalls Probleme (für die Rematerialisierung).
Aber ich stimme zu, dass es schön wäre, (zumindest) einfache Fälle von abzudecken
defensives Kopieren.
Das heißt, ein Klon zu schlagen scheint möglich und nicht besonders schwierig zu sein. Aber mit der Umstellung
int len = new int[arrayLength].length;
->
int len = arrayLength;
Schwierigkeiten entstehen:
Wir entfernen keine Array-Zuordnungen, deren Länge nicht bekannt ist
weil sie möglicherweise eine NegativeArraySize-Ausnahme verursachen. In diesem Fall wir
sollte jedoch nachweisen können, dass die Länge positiv ist.
Wie auch immer - ich habe einen fast fertigen Patch, der nicht verwendete Arrays ersetzt
Zuweisungen mit einer angemessenen Wache.
Mit anderen Worten, Sie können die Erstellung eines Arrays nicht einfach wegnehmen und wegwerfen, da die Ausführung gemäß der Spezifikation eine NegativeArraySizeException
und wir nichts dagegen tun können:
@Test void arrayWithNwgativeSize() { int length = 0; try { int newLen = -3; length = new Object[newLen].length;
Warum konnte der Gral? Ich denke, der Grund dafür ist, dass der Wert des length
im obigen Benchmark konstant und immer gleich 10 war, was den Profiler zu dem Schluss führte, dass die Überprüfung eines negativen Werts nicht erforderlich ist, was bedeutet, dass er zusammen mit der Erstellung des Arrays selbst entfernt werden kann. Korrigieren Sie in den Kommentaren, wenn ich einen Fehler gemacht habe.
Das ist alles für heute :) Fügen Sie Ihre Beispiele in die Kommentare ein, wir werden verstehen.