Das Zurücksetzen des Speichersteckplatzes erhöht den Gasverbrauch, sollte ihn jedoch verringern

Ich habe einen einfachen Vertrag, der das letzte Element des Arrays löscht:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

    function deleteLastElem() public returns(bytes32) {
        myArray.length--;
    }
}

Die Transaktionskosten für den Anruf deleteLastElem()betragen 17182 Gas.

Wenn ich es ändere zu:

pragma solidity^0.4.11;

contract GasRefundTest {

    uint[] myArray = [1, 2];

    function deleteLastElem() public returns(bytes32) {
        delete myArray[1];
        myArray.length--;
    }
}

die Transaktionskosten betragen 22480 Gas.

Ich dachte, das Löschen von Speicherplätzen sollte zu einer Gasrückerstattung führen, stattdessen sehe ich einen Gasanstieg.

Kann jemand erklären, was hier los ist.

Der beste Weg, diese Frage zu beantworten, scheint, auf niedriger Ebene zu gehen und beide Prozesse im Detail zu beschreiben (Bytecode oder Opcodes).
Ich stimme zu. Ein Blick in Bytecode oder Assembly würde helfen, die Dinge zu klären

Antworten (2)

Das Reduzieren der Größe eines Arrays mit dynamischer Größe löscht bereits die Elemente, die „entfernt“ wurden.

Die Version Ihres Codes, die zuerst a ausführt, führt delete myArray[1]also nur einen zusätzlichen Schreibvorgang in den Speicher durch, der ohnehin gleich ausgeführt wird.

Lustige Dinge zum Ausprobieren:

// This doesn't take more gas depending on how big you make the array.
myArray.length = 3; // or 300 or 3000

myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
...
// This takes more gas the more elements you're removing, because it has to zero
// out more positions in storage.
myArray.length = 9; // vs. 1

BEARBEITEN

Ich sollte darauf hinweisen, dass dieses letzte Beispiel etwas verwirrend ist. Die Gasrückerstattung tritt dort ein, ist aber auf die Hälfte des verbrauchten Benzins begrenzt.

Danke für die Antwort. Bitte erklären Sie, warum 17k Gas unmöglich ist. Ich habe in den von Ihnen erwähnten Kommentaren gesehen, dass das bezahlte Benzin mindestens 21.000 betragen sollte, aber ich habe dies nirgendwo im Yellowpaper oder in den Quellen erwähnt gefunden. Soweit ich weiß, würde die Transaktion ohne die Rückerstattung 21.000 + 5.000 + 5.000 = ~ 31.000 kosten (Eigengas + Einstellungslänge + Einstellung des letzten Elements auf 0). Die Hälfte davon sind 15,5k. Nach Erstattung dieser verbrauchten Menge wird Benzin 15500. Runden Sie um, was Remix zeigt.
In meinem vorherigen Kommentar meinte ich eine Obergrenze von 15,5.000, die größer als 15.000 ist, sodass die 15.000 zurückerstattet werden, was insgesamt 16.000 entspricht.
@medvedev1088 Ich suche eine Quelle, kann sie aber auch nicht im Yellow Paper finden. Vielleicht irre ich mich.
@medvedev1088 Du hast recht. Die einzige Grenze für Rückerstattungen ist die Hälfte des verbrauchten Gases, sodass ein Verbrauch von weniger als 21000 Gas möglich ist. (Ich habe tatsächliche Transaktionen im Mainnet mit weniger als 21000 verbrauchtem Gas beobachtet.)
Übrigens ist die Tatsache, dass die Verringerung der Array-Länge eine O(n)-Zeit anstelle der erwarteten konstanten Zeit ist und die zunehmende Array-Länge eine konstante Zeit anstelle der erwarteten O(n) ist, kontraintuitiv und sollte in der Dokumentation erwähnt werden.
Es sollte O(n)-Zeit sein, oder? Nicht O(log(n)).
Ah stimmt, Tippfehler :)
Ich stimme auch zu, dass dies in der Dokumentation erwähnt werden sollte. Ich musste experimentieren, um herauszufinden, welche Operation die Nullung durchführte. (Ich ging davon aus, dass dies der Fall sein müsste.) Angesichts der Tatsache, dass der gesamte Speicher implizit auf Null initialisiert wird, ist es jedoch sinnvoll, das Array zu verkürzen, das die Arbeit erledigen muss. (Meistens wäre es eine Verschwendung von Nullspeicherung während der Array-Erweiterung.)
Ich bin auch der Meinung, dass das Zurücksetzen unbesetzter Slots nicht die Aufgabe eines Compilers sein sollte. Es sollte besser als Bibliothekscode implementiert werden (wahrscheinlich in Assembly). In vielen Fällen möchten die Leute kontrollieren, ob und wie Slots auf Null gesetzt werden (z. B. wenn es bereits 0 ist, berühren Sie es nicht).
Ich würde argumentieren, dass es ein gutes Standardverhalten ist, wenn der Speicher beim Verkleinern eines Arrays auf Null gesetzt wird. (Alles andere wäre für den Entwickler ziemlich überraschend.) Sie könnten das Verringern immer überspringen myArray.lengthund stattdessen Ihre eigene Variable behalten, die angibt, wie viele Elemente gültig sind. Dann könnten Sie jede gewünschte Logik implementieren, wann Dinge auf Null gesetzt werden sollen.
Ich stimme zu, dass dies das Standardverhalten sein sollte. Das Verschieben vom Compiler in eine Bibliothek wäre transparenter und flexibler.
Andererseits ist das Array eine eingebaute Datenstruktur, daher ist es sinnvoll, die meisten Operationen im Compiler zu implementieren. Bei Bedarf kann ein Wrapper als Bibliothek implementiert werden, die die oben beschriebene Logik hätte.
Oder anstatt diese Logik in die Längeneigenschaft zu packen, könnten sie eine Größenänderungsfunktion für Arrays hinzufügen. Auf diese Weise wird klarer, dass dies nicht nur das Festlegen der Eigenschaft ist, sondern auch die gesamte Arbeit, die mit der Größenänderung des Arrays verbunden ist. Viele Leute erwarten nicht, dass die Einstellung der Länge einige schwere Arbeit leisten wird. In dieser Top-Antwort zum Beispiel hat der Autor nicht damit gerechnet, also hat er selbst Slot ethereum.stackexchange.com/a/1528/18932 aufgeräumt

Um es auf hohem Niveau zu erklären, Sie führen 2 Operationen statt 1 durch, daher ist mehr Gas erforderlich. Der gesamte von Ihnen geschriebene Code wird in Low-Level-Befehle der Ethereum Virtual Machine (EVM) kompiliert, die dann von ihr interpretiert werden. Für jeden dieser Befehle ist ein bestimmter Gaspreis definiert, siehe hier .

Im zweiten Fall verwenden Sie jetzt delete. Aus den Dokumenten ,

a löschen weist a den Anfangswert für den Typ zu

Es ist wichtig zu beachten, dass sich das Löschen von a wirklich wie eine Zuweisung zu a verhält, dh es speichert ein neues Objekt in a.

Sie setzen also 0zuerst dorthin, bevor Sie die Länge eines Arrays verringern. Aus reinem Interesse können Sie versuchen, myArray[1] = 0;stattdessen zu verwenden und zu sehen, wie sich dies auf das verwendete Gas auswirkt.

Aus der ursprünglichen Frage: "Ich dachte, das Löschen von Speicherplätzen sollte zu einer Gasrückerstattung führen, stattdessen sehe ich einen Gasanstieg." Siehe meine Antwort, warum dies hier nicht der Fall ist.
@smarx, ich habe deine Antwort gesehen und im Grunde reden wir über dasselbe.
Ich glaube nicht, dass es bei der Verwirrung darum ging, warum es mehr kostet, zwei Dinge zu tun als eines. Deshalb reduziert die Gasrückerstattung den Gaspreis nicht.
@smarx, war es, und meine Antwort verdeutlicht das ebenso wie deine, nicht wahr?
Ich bin jetzt völlig verwirrt. Ich bin mir nicht sicher, ob ich Ihre Antwort falsch verstehe oder Sie meine falsch verstehen.
@smarx, meine Antwort ohne den ersten Absatz liest sich genauso wie deine. Der erste Absatz erklärt nur, wie EVM im Allgemeinen funktioniert, damit der Leser versteht, warum die Zuweisung Gas kostet, anstatt es zurückzuerstatten. Vielleicht hätte ich meine Antwort anders strukturieren sollen, um die Verwirrung zu lösen.
Der Unterschied, den ich sehe, besteht darin, dass Ihre Antwort nicht darauf eingeht, warum das Schreiben einer Null den Gasverbrauch nicht so senkt, wie Sie es normalerweise erwarten würden. ( foo = 6; foo = 0;ist "2 Arbeitsgänge statt 1", verbraucht aber weniger Gas als nur foo = 6;)
@smarx, aber ich sehe auch keine Antwort darauf;) In Bezug auf Ihr Beispiel wäre es schön, von Ihnen zu hören, warum - Compiler-Optimierung? Compiler-Optimierung + kein tatsächliches Schreiben? (Falls foo vor der Zuweisung gleich 0 war)
myArray.length--Es ist die erste Zeile meiner Antwort: „Das Reduzieren der Größe eines Arrays mit dynamischer Größe löscht delete myArray[myArray.length - 1];bereits die Elemente, die ‚entfernt‘ wurden deletebringt dir nichts.
Entschuldigung, ich war in meinem foo = 6; foo = 0;Beispiel davon ausgegangen, dass foodies nicht bereits der Fall war 0.
Und ich habe es nicht mit aktivierten Compiler-Optimierungen getestet. Hoffentlich würde sich der Compiler tatsächlich nicht mit der foo = 6. Mein Punkt war nur, dass "2 Operationen teurer als 1" aufgrund von Gasrückerstattungen nicht korrekt sind. Vielleicht wäre ein deutlicheres Beispiel, foo = 6; bar = 0;weniger Gas zu verbrauchen als nur foo = 6(wiederum, wo barzuvor nicht Null war).
Warten Sie, haben Sie gesagt, dass das Überschreiben von 2 Variablen weniger Gas verbraucht als das Überschreiben von 1? Ich kann mir das nicht vorstellen. Könnten Sie bitte mehr erklären?
Und trotzdem kann ich nicht verstehen, was Sie genau unter "Gasrückerstattung" meinen. Wenn eine solche „Gasrückerstattung“ möglich ist, was ist, wenn ich eine Methode anwende, die davon profitiert? Wie set variable – geben Sie sie frei und erhalten Sie eine Rückerstattung – setzen Sie sie erneut – geben Sie sie erneut frei und so weiter. Klingt so, als könnte eine virtuelle Maschine hier stecken bleiben, wenn sie eine so gut gestaltete „Gasrückerstattungs“ -Schleife erhält. Oder übersehe ich etwas? Ich freue mich darauf, es zu erfahren.
Ja, das ist der Sinn dieser Frage. :-) Das Schreiben einer Null kostet 5000 Gas, und wenn dort vorher eine Nicht-Null war, bekommt man 15000 Gas zurückerstattet. Gasrückerstattungen werden kumuliert und können bis zur Hälfte des durch die Transaktion verbrauchten Gases ausgeglichen werden. (Beachten Sie jedoch, dass es eine Mindesttransaktionskosten gibt: 21000.) Die Mathematik einer Gasrückerstattung ist also etwa so gas_consumed = max(21000, gas_consumed - min(gas_consumed / 2, gas_refund)).
Jede Iteration von x = 5; x = 0;in einer Schleife würde 20.000 (Kosten für das Schreiben einer Nicht-Null, wo zuvor eine Null war) + 5.000 (Kosten für das Schreiben einer Null) an Gas kosten und würde 15.000 zur Gasrückerstattung hinzufügen. Damit wäre kein Gewinn zu erzielen.
Macht nichts, @smarx, wenn ich diesen Artikel sorgfältig durchlese, habe ich verstanden, wovon du gesprochen hast. Danke, dass Sie hier bleiben!
@smarx schnelle Errata-Notiz, Ihre Gasverbrauchsformel ist deaktiviert, da die Gaskosten mit Rückerstattungen unter 21000 sinken können!
@ZitRo der Übersichtlichkeit halber, wenn der Array-Wert im Konstruktor zugewiesen wird, dann deletewürde die Position nicht mehr unbedingt auf 0 gesetzt werden, sondern auf den Anfangswert.