Von Anfang an, als ich mit dem Programmieren anfing, stellte sich die Frage, was zur Verbesserung der Leistung verwendet werden sollte: Struktur oder Klasse; Welche Arrays sind besser zu verwenden und wie. In Bezug auf Strukturen begrüßt Apple ihre Verwendung und erklärt, dass sie besser optimiert werden können und die gesamte Essenz der Swift-Sprache Strukturen sind. Es gibt jedoch diejenigen, die damit nicht einverstanden sind, da Sie den Code auf wunderbare Weise vereinfachen können, indem Sie eine Klasse von einer anderen erben und mit einer solchen Klasse arbeiten. Um die Arbeit mit Klassen zu beschleunigen, haben wir verschiedene Modifikatoren und Objekte erstellt, die speziell für Klassen optimiert wurden. Es ist bereits schwierig zu sagen, was in welchem Fall schneller sein wird.
Um alle Punkte auf dem „e“ anzuordnen, habe ich mehrere Tests geschrieben, die die üblichen Ansätze für die Datenverarbeitung verwenden: Übergabe an eine Methode, Kopieren, Arbeiten mit Arrays usw. Ich habe beschlossen, keine großen Schlussfolgerungen zu ziehen. Jeder wird selbst entscheiden, ob es sich lohnt, an die Tests zu glauben, das Projekt herunterladen und sehen, wie es für Sie funktioniert, und versuchen, den Betrieb eines bestimmten Tests zu optimieren. Vielleicht kommen sogar neue Chips heraus, die ich nicht erwähnt habe, oder sie werden so selten verwendet, dass ich einfach nichts davon gehört habe.
PS Ich habe angefangen, an einem Artikel über Xcode 10.3 zu arbeiten, und ich habe darüber nachgedacht, die Geschwindigkeit mit Xcode 11 zu vergleichen. In diesem Artikel geht es jedoch nicht um den Vergleich zweier Anwendungen, sondern um die Geschwindigkeit unserer Anwendungen. Ich habe keinen Zweifel daran, dass die Laufzeit von Funktionen abnimmt und die schlecht optimierte schneller wird. Infolgedessen wartete ich auf den neuen Swift 5.1 und beschloss, die Hypothesen in der Praxis zu testen. Viel Spaß beim Lesen.
Test 1: Vergleichen Sie Arrays auf Strukturen und Klassen
Angenommen, wir haben eine Klasse und möchten die Objekte dieser Klasse in ein Array einfügen. Die übliche Aktion für ein Array besteht darin, sie zu durchlaufen.
Wenn in einem Array Klassen verwendet werden und versucht wird, durch das Array zu gehen, erhöht sich die Anzahl der Verknüpfungen. Nach Abschluss verringert sich die Anzahl der Verknüpfungen zum Objekt.
Wenn wir die Struktur durchgehen, wird zum Zeitpunkt des Aufrufs des Objekts durch den Index eine Kopie des Objekts erstellt, die denselben Speicherbereich betrachtet, jedoch als unveränderlich markiert ist. Es ist schwer zu sagen, was schneller ist: Erhöhen Sie die Anzahl der Verknüpfungen zu einem Objekt oder erstellen Sie eine Verknüpfung zu einem Bereich im Speicher, da diese nicht geändert werden kann. Lassen Sie es uns in der Praxis überprüfen:
Abb. 1: Vergleich des Abrufens einer Variablen aus Arrays basierend auf Strukturen und KlassenTest 2. Vergleichen Sie ContiguousArray mit Array
Interessanter ist es, die Leistung eines Arrays (Arrays) mit einem Referenzarray (ContiguousArray) zu vergleichen, das speziell für die Arbeit mit im Array gespeicherten Klassen benötigt wird.
Lassen Sie uns die Leistung für die folgenden Fälle überprüfen:
ContiguousArray speichert eine Struktur mit dem Werttyp
ContiguousArray speichert Struktur mit String
ContiguousArray-Speicherklasse mit Werttyp
ContiguousArray-Speicherklasse mit String
Array-Speicherstruktur mit Werttyp
Array-Speicherstruktur mit String
Array-Speicherklasse mit Werttyp
Array-Speicherklasse mit String
Da die Testergebnisse (Tests: Übergabe an eine Funktion mit deaktivierter Inline-Optimierung, Übergabe an eine Funktion mit aktivierter Inline-Optimierung, Löschen von Elementen, Hinzufügen von Elementen, sequentieller Zugriff auf ein Element in einer Schleife) eine große Anzahl von Tests enthalten (für 8 Arrays mit jeweils 5 Tests) Ich werde die wichtigsten Ergebnisse geben:
- Wenn Sie eine Funktion aufrufen und ein Array übergeben und sie inline deaktivieren, ist ein solcher Aufruf sehr teuer (für Klassen, die auf der Referenzzeichenfolge basieren, ist er 20.000-mal langsamer, für Klassen, die auf Value basieren, ist der Typ 60.000-mal langsamer, schlechter, wenn der Inline-Optimierer deaktiviert ist). .
- Wenn die Optimierung (Inline) für Sie funktioniert, sollte eine Verschlechterung nur zweimal erwartet werden, je nachdem, welcher Datentyp zu welchem Array hinzugefügt wird. Die einzige Ausnahme war der Werttyp, der in eine im ContiguousArray liegende Struktur eingebunden war - ohne zeitliche Verschlechterung.
- Entfernung - Die Streuung zwischen dem Referenzarray und dem üblichen Array betrug etwa 20% (zugunsten des üblichen Arrays).
- Anhängen - Bei Verwendung von in Klassen eingeschlossenen Objekten war ContiguousArray mit denselben Objekten etwa 20% schneller als Array, während Array bei der Arbeit mit Strukturen schneller war als ContiguousArray mit Strukturen.
- Der Zugriff auf Array-Elemente bei Verwendung von Wrappern aus Strukturen erwies sich als schneller als alle Wrapper über Klassen, einschließlich ContiguousArray (etwa 500-mal schneller).
In den meisten Fällen ist die Verwendung regulärer Arrays für die Arbeit mit Objekten effizienter. Vorher verwendet, verwenden wir weiter.
Die Schleifenoptimierung für Arrays wird vom Lazy Collection-Initialisierer bereitgestellt, mit dem Sie nur einmal über das gesamte Array laufen können, selbst wenn mehrere Filter oder Maps für Array-Elemente verwendet werden.
Bei der Verwendung von Strukturen als Optimierungswerkzeug gibt es Fallstricke, z. B. die Verwendung von Typen, auf die intern verwiesen wird: Zeichenfolgen, Wörterbücher, Referenzarrays. Wenn dann eine Variable, die einen Referenztyp in sich speichert, in eine Funktion eingegeben wird, wird für jedes Element, das eine Klasse ist, eine zusätzliche Referenz erstellt. Dies hat eine andere Seite, etwas weiter. Sie können versuchen, eine Wrapper-Klasse über einer Variablen zu verwenden. Dann erhöht sich die Anzahl der Verknüpfungen beim Übergeben an die Funktion nur für diese, und die Anzahl der Verknüpfungen zu den Werten innerhalb der Struktur bleibt gleich. Im Allgemeinen möchte ich sehen, wie viele Variablen eines Referenztyps in der Struktur enthalten sein müssen, damit seine Leistung geringer abnimmt als die Leistung von Klassen mit denselben Parametern. Im Internet gibt es einen Artikel mit dem Titel „Stop Using Structs!“, In dem dieselbe Frage gestellt und beantwortet wird. Ich habe das Projekt heruntergeladen und beschlossen herauszufinden, was wo passiert und in welchen Fällen wir langsame Strukturen bekommen. Der Autor zeigt die geringe Leistung von Strukturen im Vergleich zu Klassen und argumentiert, dass das Erstellen eines neuen Objekts viel langsamer ist als das Erhöhen des Verweises auf das Objekt absurd (daher habe ich die Zeile entfernt, in der jedes Mal ein neues Objekt in der Schleife erstellt wird). Wenn wir jedoch keine Verknüpfung zum Objekt erstellen, sondern es einfach an eine Funktion übergeben, um damit zu arbeiten, ist der Leistungsunterschied sehr unbedeutend. Jedes Mal, wenn wir eine Funktion
inline (nie) setzen, muss unsere Anwendung sie ausführen und darf keinen Code in einer Zeichenfolge erstellen. Nach den Tests hat Apple dafür gesorgt, dass das an die Funktion übergebene Objekt geringfügig geändert wird. Bei Strukturen ändert der Compiler die Veränderbarkeit und macht den Zugriff auf nicht veränderbare Eigenschaften des Objekts verzögert. Ähnliches passiert in der Klasse, erhöht aber gleichzeitig die Anzahl der Verweise auf das Objekt. Und jetzt haben wir ein faules Objekt, alle seine Felder sind auch faul und jedes Mal, wenn wir eine Objektvariable aufrufen, wird sie initialisiert. Dabei sind Strukturen nicht gleich: Wenn eine Funktion zwei Variablen aufruft, ist die Struktur des Objekts der Geschwindigkeitsklasse nur geringfügig unterlegen; Wenn Sie drei oder mehr aufrufen, ist die Struktur immer schneller.
Test 3: Vergleichen Sie die Leistung von Strukturen und Klassen, in denen große Klassen gespeichert sind
Außerdem bin ich ein wenig verändert sich Methode, die aufgerufen wird, wenn Sie eine weitere Variable hinzu (und somit in dem Verfahren initialisiert drei Variablen, und nicht zwei wie in dem Artikel), und dass es kein Überlauf Int Ersetzen Operationen auf Variablen in der Summe und Subtraktion. Verständlichere Zeitmetriken hinzugefügt (im Screenshot sind es Sekunden, aber es ist nicht so wichtig für uns, das Verständnis der resultierenden Proportionen ist wichtig), Entfernen des Darwin-Frameworks (ich verwende es nicht in Projekten, möglicherweise vergeblich, es gibt keine Unterschiede in den Tests vor / nach dem Hinzufügen des Frameworks in meinem Test). die Einbeziehung maximaler Optimierung und Aufbau auf dem Release-Build (es scheint, dass dies ehrlicher sein wird), und hier ist das Ergebnis:
Abb. 2: Leistung von Strukturen und Klassen aus dem Artikel „Stop Using Structs“Die Unterschiede in den Testergebnissen sind vernachlässigbar.
Test 4: Funktion, die Generika, Protokolle und Funktionen ohne Generika akzeptiert
Wenn wir eine generische Funktion übernehmen und dort zwei Werte übergeben, die nur durch die Möglichkeit zum Vergleichen dieser Werte (func min) vereint sind, wird der Code aus drei Zeilen in einen Code aus acht (wie Apple sagt). Dies ist jedoch nicht immer der Fall. Xcode verfügt über Optimierungsmethoden, bei denen beim Aufrufen einer Funktion, wenn zwei Strukturwerte an Xcode übergeben werden, automatisch eine Funktion generiert wird, die zwei Strukturen akzeptiert und die Werte nicht mehr kopiert.
Abb. 3: Typische generische FunktionIch habe beschlossen, zwei Funktionen zu testen: In der ersten wird der generische Datentyp deklariert, in der zweiten wird nur das Protokoll akzeptiert. In der neuen Version des Swift 5.1-Protokolls ist es sogar etwas schneller als Generic (vor Swift 5.1 waren die Protokolle zweimal langsamer), obwohl es laut Apple umgekehrt sein sollte, aber wenn es um das Durchlaufen eines Arrays geht, müssen wir bereits tippen, was langsamer wird Generisch (aber sie sind immer noch großartig, weil sie schneller als Protokolle sind):
Abb. 4: Vergleich der generischen und Protokoll-Hostfunktionen.Test 5: Vergleichen Sie den Aufruf der übergeordneten Methode mit der nativen und überprüfen Sie gleichzeitig die letzte Klasse auf einen solchen Aufruf
Was mich immer interessiert hat, ist, wie langsam Klassen mit einer großen Anzahl von Eltern arbeiten, wie schnell eine Klasse ihre Funktionen und die eines Elternteils aufruft. In Fällen, in denen wir versuchen, eine Methode aufzurufen, die eine Klasse akzeptiert, kommt der dynamische Versand ins Spiel. Was ist das? Jedes Mal, wenn eine Methode oder Variable in unserer Funktion aufgerufen wird, wird eine Nachricht generiert, in der das Objekt nach dieser Variablen oder Methode gefragt wird. Das Objekt, das eine solche Anforderung empfängt, beginnt die Suche nach der Methode in der Versandtabelle seiner Klasse. Wenn eine Überschreibung der Methode oder Variablen aufgerufen wurde, nimmt es diese und gibt sie zurück oder erreicht rekursiv die Basisklasse.
Abb. 5: Klassenmethodenaufrufe für VersandtestsAus dem obigen Test können mehrere Schlussfolgerungen gezogen werden: Je größer die Klasse der übergeordneten Klassen ist, desto langsamer arbeitet sie und der Geschwindigkeitsunterschied ist so gering, dass er sicher vernachlässigt werden kann. Die Codeoptimierung führt höchstwahrscheinlich dazu, dass es keinen Geschwindigkeitsunterschied gibt. In diesem Beispiel hat der endgültige Klassenmodifikator keinen Vorteil, im Gegenteil, die Arbeit der Klasse ist noch langsamer, möglicherweise aufgrund der Tatsache, dass sie keine wirklich schnelle Funktion wird.
Test 6: Aufrufen einer Variablen mit dem letzten Modifikator gegen eine reguläre Klassenvariable
Auch sehr interessante Ergebnisse beim Zuweisen des endgültigen Modifikators zu einer Variablen können Sie verwenden, wenn Sie sicher sind, dass die Variable nirgendwo in den Erben der Klasse neu geschrieben wird. Versuchen wir, den endgültigen Modifikator einer Variablen zuzuweisen. Wenn wir in unserem Test nur eine Variable erstellt und eine Eigenschaft darauf aufgerufen haben, wird sie einmal initialisiert (das Ergebnis ist von unten). Wenn wir jedes Mal ehrlich ein neues Objekt erstellen und dessen Variable anfordern, verlangsamt sich die Geschwindigkeit merklich (das Ergebnis ist oben):
Abb. 6: Letzte Variable aufrufenOffensichtlich hat sich der Modifikator nicht für die Variable entschieden, und er ist immer langsamer als sein Konkurrent.
Test 7: Problem des Polymorphismus und Protokolle für Strukturen. Oder die Leistung eines existentiellen Containers
Problem: Wenn wir ein Protokoll verwenden, das eine bestimmte Methode und mehrere von diesem Protokoll geerbte Strukturen unterstützt, was wird unser Compiler denken, wenn wir Strukturen mit unterschiedlichen Volumina gespeicherter Werte in ein Array einfügen, das durch das ursprüngliche Protokoll vereint wird?
Um das Problem des Aufrufs einer in den Erben vordefinierten Methode zu lösen, wird der Mechanismus der Protokollzeugen-Tabelle verwendet. Es werden Shell-Strukturen erstellt, die auf die erforderlichen Methoden verweisen.
Um das Problem der Datenspeicherung zu lösen, wird ein Existenzcontainer verwendet. Es speichert in sich 5 Informationszellen mit jeweils 8 Bytes. In den ersten drei Fällen wird Speicherplatz für die in der Struktur gespeicherten Daten zugewiesen (wenn sie nicht passen, wird eine Verknüpfung zu dem Heap hergestellt, in dem die Daten gespeichert sind), in der vierten werden Informationen zu den in der Struktur verwendeten Datentypen gespeichert und die Verwaltung dieser Daten erläutert Der fünfte enthält Verweise auf die Methoden des Objekts.
Abbildung 7. Vergleich der Leistung eines Arrays, das eine Verknüpfung zu einem Objekt erstellt und dieses enthältZwischen dem ersten und dem zweiten Ergebnis hat sich die Anzahl der Variablen verdreifacht. Theoretisch sollten sie in einen Behälter gegeben werden, sie werden in diesem Behälter aufbewahrt, und der Geschwindigkeitsunterschied ist auf das Volumen der Struktur zurückzuführen. Interessanterweise ändert sich die Betriebszeit nicht, wenn Sie die Anzahl der Variablen in der zweiten Struktur reduzieren, dh der Container speichert tatsächlich 3 oder 2 Variablen. Es scheint jedoch spezielle Bedingungen für eine Variable zu geben, die die Geschwindigkeit erheblich erhöhen. Die zweite Struktur passt perfekt in den Behälter und unterscheidet sich im Volumen von der dritten um die Hälfte, was zu einer starken Verschlechterung der Laufzeit im Vergleich zu anderen Strukturen führt.
Ein bisschen Theorie zur Optimierung Ihrer Projekte
Folgende Faktoren können die Leistung von Strukturen beeinflussen:
- wo seine Variablen gespeichert sind (Heap / Stack);
- die Notwendigkeit der Referenzzählung für Eigenschaften;
- Planungsmethoden (statisch / dynamisch);
- Copy-On-Write wird nur von Datenstrukturen verwendet, bei denen es sich um Referenztypen handelt, die vorgeben, Strukturen (String, Array, Set, Dictionary) unter der Haube zu sein.
Es sollte sofort klargestellt werden, dass das schnellste Objekt Objekte sind, die Eigenschaften im Stapel speichern. Verwenden Sie bei der statischen Methode der medizinischen Untersuchung keine Referenzzählung.
Dann sind Klassen im Vergleich zu Strukturen schlecht und gefährlich
Wir kontrollieren nicht immer das Kopieren unserer Objekte, und wenn wir dies tun, können wir zu viele Kopien erhalten, die schwierig zu verwalten sind (wir haben im Projekt Objekte erstellt, die beispielsweise für die Erstellung der Ansicht verantwortlich sind).
Sie sind nicht so schnell wie Strukturen.
Wenn wir einen Link zu einem Objekt haben und versuchen, unsere Anwendung in einem Multithread-Stil zu steuern, können wir die Race-Bedingung erhalten, wenn unser Objekt von zwei verschiedenen Stellen aus verwendet wird (und dies ist nicht so schwierig, da ein mit Xcode erstelltes Projekt immer etwas langsamer ist). als Store-Version).
Wenn wir versuchen, die Race-Bedingung zu vermeiden, geben wir eine Menge Ressourcen für Lock und unsere Daten aus, was anfängt, Ressourcen zu verbrauchen und Zeit zu verschwenden, anstatt schnell zu verarbeiten, und wir erhalten noch langsamere Objekte als dieselben, die auf Strukturen basieren.
Wenn wir alle oben genannten Aktionen an unseren Objekten (Links) ausführen, ist die Wahrscheinlichkeit unvorhergesehener Deadlocks hoch.
Die Komplexität des Codes nimmt dadurch zu.
Immer mehr Code = mehr Bugs!
Schlussfolgerungen
Ich dachte, dass die Schlussfolgerungen in diesem Artikel einfach notwendig sind, weil ich den Artikel nicht von Zeit zu Zeit lesen möchte und eine konsolidierte Liste von Punkten einfach notwendig ist. Ich möchte die Zeilen unter den Tests zusammenfassen und Folgendes hervorheben:
- Arrays werden am besten in einem Array platziert.
- Wenn Sie ein Array aus Klassen erstellen möchten, ist es besser, ein reguläres Array auszuwählen, da ContiguousArray selten Vorteile bietet und diese nicht sehr hoch sind.
- Inline-Optimierung beschleunigt die Arbeit, schalten Sie sie nicht aus.
- Der Zugriff auf Array-Elemente ist immer schneller als der Zugriff auf ContiguousArray-Elemente.
- Strukturen sind immer schneller als Klassen (es sei denn, Sie aktivieren natürlich die Optimierung des gesamten Moduls oder eine ähnliche Optimierung).
- Wenn ein Objekt an die Funktion übergeben und rufen Sie die Eigenschaften aus der dritten, die Struktur schneller Klassen.
- Wenn Sie einen Wert an eine für Generic und Protocol geschriebene Funktion übergeben, ist Generic schneller.
- Bei der Vererbung mehrerer Klassen nimmt die Geschwindigkeit des Funktionsaufrufs ab.
- Variablen kennzeichnen die endgültige Arbeit langsamer als normale Paprikaschoten.
- Wenn eine Funktion ein Objekt akzeptiert, das mehrere Objekte mit dem Protokoll kombiniert, funktioniert es schnell, wenn nur eine Eigenschaft darin gespeichert ist, und wird beim Hinzufügen weiterer Eigenschaften erheblich beeinträchtigt.
Referenzen:
medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1developer.apple.com/videos/play/wwdc2016/416developer.apple.com/videos/play/wwdc2015/409developer.apple.com/videos/play/wwdc2016/419medium.com/commencis/stop-using-structs-e1be9a86376fQuellcode testen