„Programmierer verbringen viel Zeit damit, sich Gedanken über die Geschwindigkeit ihrer Programme zu machen, und Versuche, Effizienz zu erzielen, wirken sich oft dramatisch negativ auf die Fähigkeit aus, sie zu debuggen und zu unterstützen. Kleine Optimierungen müssen beispielsweise in 97% der Fälle vergessen werden. Vorzeitige Optimierung ist die Wurzel allen Übels! Aber wir dürfen die 3% nicht aus den Augen verlieren, bei denen es wirklich wichtig ist! “
Donald Knut.

Bei der Prüfung intelligenter Verträge fragen wir uns manchmal, ob sich ihre Entwicklung auf diejenigen 97% bezieht, bei denen keine Überlegungen zur Optimierung erforderlich sind, oder ob es sich nur um die 3% der Fälle handelt, in denen dies wichtig ist. Unserer Meinung nach eher die zweite. Im Gegensatz zu anderen Anwendungen werden intelligente Verträge nicht aktualisiert und können nicht „unterwegs“ optimiert werden (vorausgesetzt, ihr Algorithmus ist nicht festgelegt, dies ist jedoch ein separates Thema). Das zweite Argument für eine
frühzeitige Vertragsoptimierung ist, dass im Gegensatz zu den meisten Systemen, bei denen sich die Suboptimalität nur auf einer Skala manifestiert, die sich auf die Besonderheiten der Hardware und Umgebung bezieht, anhand einer Vielzahl von Metriken gemessen wird, ein intelligenter Vertrag im Wesentlichen die einzige Leistungsmetrik hat - den Gasverbrauch.
Daher ist es technisch einfacher, die Wirksamkeit eines Vertrags zu bewerten, aber Entwickler verlassen sich häufig weiterhin auf ihre Intuition und führen dieselbe blinde „vorzeitige Optimierung“ durch, über die Professor Knut gesprochen hat. Wir werden anhand des Beispiels der Auswahl der Bittiefe einer Variablen überprüfen, wie intuitiv die Lösung der Realität entspricht. In diesem Beispiel werden wir, wie in den meisten praktischen Fällen, keine Einsparungen erzielen, und sogar umgekehrt wird sich unser Vertrag im Hinblick auf den Gasverbrauch als teurer herausstellen.
Was für ein Gas?
Ethereum ist wie ein globaler Computer, dessen „Prozessor“ die virtuelle EVM-Maschine ist, dessen „Programmcode“ eine Folge von Befehlen und Daten ist, die in einem intelligenten Vertrag aufgezeichnet sind, und Anrufe sind Transaktionen von außen. Transaktionen werden in verwandte Strukturen gepackt - Blöcke, die alle paar Sekunden auftreten. Und da die Blockgröße per Definition begrenzt ist und das Verarbeitungsprotokoll deterministisch ist (eine einheitliche Verarbeitung aller Transaktionen im Block durch alle Knoten des Netzwerks erfordert), muss das System einen fairen Algorithmus für die Auswahl der zu bedienenden Anforderung bereitstellen, um eine potenziell unbegrenzte Nachfrage mit einer begrenzten Knotenressource zu befriedigen und vor DoS zu schützen. und dessen Ignorieren gibt es als Mechanismus in vielen öffentlichen Blockchains ein einfaches Prinzip: Der Absender kann die Höhe der Vergütung für den Bergmann für die Durchführung seiner Transaktion auswählen ktsii und Bergmann wählt, deren Bedürfnisse umfassen einen Block, und dessen nicht, die profitabelsten für sich entschieden haben.
In Bitcoin, wo der Block auf ein Megabyte begrenzt ist, entscheidet der Miner beispielsweise, ob die Transaktion in den Block aufgenommen werden soll oder nicht, basierend auf ihrer Länge und der vorgeschlagenen Provision (Auswahl derjenigen mit dem maximalen Verhältnis von Satoshis pro Byte).
Für das komplexere Ethereum-Protokoll ist dieser Ansatz nicht geeignet, da ein einzelnes Byte sowohl das Fehlen einer Operation (z. B. des STOP-Codes) als auch die teure und langsame Schreiboperation in den Speicher (SSTORE) darstellen kann. Daher wird für jeden in der Luft befindlichen Op-Code abhängig vom Ressourcenverbrauch ein eigener Preis angegeben.
Gebührenplan aus Protokollspezifikation
Tabelle des Gasflusses für verschiedene Betriebsarten. Aus der Protokollspezifikation des
Ethereum Yellow Paper .
Im Gegensatz zu Bitcoin legt der Absender der Ethereum-Transaktion nicht die Provision in Kryptowährung fest, sondern die maximale Menge an Gas, die er bereit ist auszugeben -
startGas und den Preis pro
Gaseinheit -
gasPreis . Wenn die virtuelle Maschine den Code ausführt, wird die Gasmenge für jede nachfolgende Operation von startGas abgezogen, bis entweder der Ausgang des Codes erreicht ist oder das Gas ausgeht. Anscheinend wird deshalb für diese Arbeitseinheit ein so seltsamer Name verwendet - die Transaktion ist wie ein Auto mit Benzin gefüllt und erreicht den Zielpunkt oder nicht, hängt davon ab, ob genügend Volumen im Tank gefüllt ist. Nach Abschluss der Codeausführung wird die Luftmenge, die durch Multiplizieren des tatsächlich verbrauchten Gases mit dem vom Absender festgelegten Preis (
Wei pro Gas) empfangen wird, vom Transaktionssender abgebucht. Im globalen Netzwerk geschieht dies zum Zeitpunkt des "Mining" des Blocks, der die entsprechende Transaktion enthält, und in der Remix-Umgebung wird die Transaktion sofort, kostenlos und ohne Bedingungen "abgebaut".
Unser Tool - Remix IDE
Für die „Profilerstellung“ des Gasverbrauchs verwenden wir die Online-Umgebung zur Entwicklung der Ethereum-Verträge der
Remix-IDE . Diese IDE enthält einen Syntax-Hervorhebungscode-Editor, einen Artefakt-Viewer, ein Rendering der Vertragsschnittstelle, einen visuellen Debugger für virtuelle Maschinen, JS-Compiler aller möglichen Versionen und viele andere wichtige Tools. Ich empfehle dringend, mit ihm das Ätherstudium zu beginnen. Ein weiteres Plus ist, dass keine Installation erforderlich ist - öffnen Sie es einfach in einem Browser von der
offiziellen Website .
Auswahl des Variablentyps
Die Spezifikation der Solidity-Sprache bietet dem Entwickler bis zu zweiunddreißig Bits ganzzahliger Typen - von 8 bis 256 Bit. Stellen Sie sich vor, Sie entwickeln einen intelligenten Vertrag, in dem das Alter einer Person in Jahren gespeichert wird. Welche Bittiefe wählen Sie?
Es wäre ganz natürlich, den für eine bestimmte Aufgabe ausreichend ausreichenden Typ zu wählen - uint8 würde hier mathematisch passen. Es wäre logisch anzunehmen, dass je kleiner das Objekt ist, das wir in der Blockchain speichern, und je weniger Speicher wir für die Ausführung ausgeben, desto weniger Overhead haben wir, desto weniger zahlen wir. In den meisten Fällen ist diese Annahme jedoch falsch.
Für das Experiment nehmen wir den einfachsten Vertrag aus dem Angebot der offiziellen
Solidity-Dokumentation und sammeln ihn in zwei Versionen - unter Verwendung des Variablentyps uint256 und des 32-mal kleineren Typs - uint8.
simpleStorage_uint256.sol pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
simpleStorage_uint8.sol pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData; function set(uint8 x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
Messung von „Einsparungen“
Daher werden Verträge erstellt, in Remix geladen, bereitgestellt und Aufrufe von .set () -Methoden von Transaktionen ausgeführt. Was sehen wir?
Das Aufzeichnen eines langen Typs ist teurer als ein kurzer - 20464 gegenüber 20205 Gaseinheiten! Wie? Warum? Lass es uns herausfinden!

Speichern Sie uint8 vs uint256
Das Schreiben in einen dauerhaften Speicher ist aus offensichtlichen Gründen eine der teuersten Vorgänge im Protokoll: Erstens erhöht das Aufzeichnen des Status die Größe des vom vollständigen Knoten benötigten Speicherplatzes. Die Größe dieses Speichers nimmt ständig zu. Je mehr Zustände an den Knoten gespeichert werden, desto langsamer erfolgt die Synchronisierung, desto höher ist die Infrastrukturanforderung (Partitionsgröße, Anzahl der Iops). In Spitzenzeiten bestimmen die langsamen Festplatten-E / A-Vorgänge die Leistung des gesamten Netzwerks.
Es wäre logisch zu erwarten, dass die Speicherung von uint8 zehnmal billiger sein sollte als die von uint256. Im Debugger können Sie jedoch sehen, dass sich beide Werte im Speichersteckplatz genauso befinden wie ein 256-Bit-Wert.

In diesem speziellen Fall bietet die Verwendung von uint8 keinen Vorteil bei den Kosten für das Schreiben in den Speicher.
Umgang mit uint8 vs uint256
Vielleicht erhalten wir Vorteile bei der Arbeit mit uint8, wenn nicht während der Speicherung, dann zumindest bei der Bearbeitung von Daten im Speicher? Im Folgenden werden die Anweisungen für dieselbe Funktion verglichen, die für verschiedene Arten von Variablen erhalten wurden.

Sie können sehen, dass Operationen mit uint8 noch
mehr Anweisungen enthalten als uint256. Dies liegt daran, dass der Computer den 8-Bit-Wert in ein natives 256-Bit-Wort konvertiert und der Code daher von zusätzlichen Anweisungen umgeben ist, für die der Absender bezahlt. In diesem Fall ist es teurer, nicht nur Code mit dem Typ uint8 zu schreiben, sondern auch auszuführen.
Wo kann die Verwendung von Kurztypen gerechtfertigt sein?
Unser Team befasst sich seit langem mit der Prüfung intelligenter Verträge, und bisher gab es keinen einzigen praktischen Fall, in dem die Verwendung eines kleinen Typs in dem für die Prüfung vorgesehenen Code zu Einsparungen führen würde. In einigen sehr spezifischen Fällen sind Einsparungen theoretisch möglich. Wenn Ihr Vertrag beispielsweise eine große Anzahl kleiner Statusvariablen oder -strukturen speichert, können diese in weniger Speichersteckplätze gepackt werden.
Der Unterschied wird am deutlichsten im folgenden Beispiel deutlich:
1. Vertrag mit 32 Variablen uint256
simpleStorage_32x_uint256.sol pragma solidity ^0.4.0; contract SimpleStorage { uint storedData1; uint storedData2; uint storedData3; uint storedData4; uint storedData5; uint storedData6; uint storedData7; uint storedData8; uint storedData9; uint storedData10; uint storedData11; uint storedData12; uint storedData13; uint storedData14; uint storedData15; uint storedData16; uint storedData17; uint storedData18; uint storedData19; uint storedData20; uint storedData21; uint storedData22; uint storedData23; uint storedData24; uint storedData25; uint storedData26; uint storedData27; uint storedData28; uint storedData29; uint storedData30; uint storedData31; uint storedData32; function set(uint x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } }
2. Vertrag mit 32 uint8-Variablen
simpleStorage_32x_uint8.sol pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData1; uint8 storedData2; uint8 storedData3; uint8 storedData4; uint8 storedData5; uint8 storedData6; uint8 storedData7; uint8 storedData8; uint8 storedData9; uint8 storedData10; uint8 storedData11; uint8 storedData12; uint8 storedData13; uint8 storedData14; uint8 storedData15; uint8 storedData16; uint8 storedData17; uint8 storedData18; uint8 storedData19; uint8 storedData20; uint8 storedData21; uint8 storedData22; uint8 storedData23; uint8 storedData24; uint8 storedData25; uint8 storedData26; uint8 storedData27; uint8 storedData28; uint8 storedData29; uint8 storedData30; uint8 storedData31; uint8 storedData32; function set(uint8 x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } }
Die Bereitstellung des ersten Vertrags (32 uint256) kostet weniger - nur 89941 Gas, aber .set () ist seitdem viel teurer Es wird 256 Speicherplätze belegen, was für jeden Anruf 640.639 Gas kostet. Der zweite Vertrag (32 uint8) wird bei der Bereitstellung (221663 Gas) zweieinhalb Mal teurer sein, aber jeder Aufruf der .set () -Methode ist viel billiger, weil ändert nur eine Zelle der Stufe (185291 Gas).
Sollte eine solche Optimierung angewendet werden?
Wie bedeutend der Effekt der Typoptimierung ist, ist ein strittiger Punkt. Wie Sie sehen, haben wir selbst für einen solchen speziell ausgewählten synthetischen Fall
keine mehrfachen Unterschiede festgestellt. Die Wahl von uint8 oder uint256 ist eher ein Beispiel dafür, dass die Optimierung entweder sinnvoll angewendet werden sollte (mit Verständnis für Werkzeuge, Profilerstellung) oder überhaupt nicht darüber nachdenken sollte. Hier sind einige allgemeine Richtlinien:
- Wenn der Vertrag viele kleine Zahlen oder kompakte Strukturen im Repository enthält, können Sie über eine Optimierung nachdenken.
- Wenn Sie den Typ "abgekürzt" verwenden, denken Sie an Über- / Unterlauf-Schwachstellen .
- Für Speichervariablen und Funktionsargumente, die nicht in das Repository geschrieben wurden, ist es immer besser, den nativen Typ uint256 (oder dessen Alias uint) zu verwenden. Zum Beispiel macht es keinen Sinn, den Listeniterator auf uint8 zu setzen - verlieren Sie einfach;
- Von großer Bedeutung für die korrekte Verpackung in Speicherplätzen für den Compiler ist die Reihenfolge der Variablen im Vertrag .
Referenzen
Am Ende werde ich Ratschläge geben, die keine Kontraindikationen haben: Experimentieren Sie mit Entwicklungswerkzeugen, kennen Sie die Spezifikationen der Sprache, Bibliothek und Frameworks. Hier sind meiner Meinung nach die nützlichsten Links, um mehr über die Ethereum-Plattform zu erfahren:
- Die Remix- Vertragsentwicklungsumgebung ist eine sehr funktionale browserbasierte IDE.
- Bei der Angabe der Solidity-Sprache wird der Link speziell auf den Abschnitt zum Layout von Statusvariablen verweisen.
- Ein sehr interessantes Vertrags- Repository des berühmten OpenZeppelin-Teams. Beispiele für die Implementierung von Token, Crowdsale-Verträgen und vor allem die SafeMath- Bibliothek, mit deren Hilfe sicher mit Typen gearbeitet werden kann;
- Ethereum Yellow Paper , formale Spezifikation der virtuellen Ethereum-Maschine;
- Ethereum White Paper , die Spezifikation der Ethereum-Plattform, ein allgemeineres und übergeordnetes Dokument mit einer großen Anzahl von Links;
- Ethereum in 25 Minuten , eine kurze, aber dennoch umfangreiche technische Einführung in Ethereum durch den Schöpfer der Plattform, Vitalik Buterin;
- Etherscan Blockchain Explorer , ein Fenster in die reale Ätherwelt , ein Browser mit Blöcken, Transaktionen, Token und Verträgen im Hauptnetzwerk. Auf Etherscan finden Sie Explorer für Testnetzwerke Rinkeby, Ropsten, Kovan (Netzwerke mit kostenlosem Broadcast, die auf verschiedenen Konsensprotokollen basieren).