Deep EVM #6: Yul-Speicherverwaltung — mstore, mload und Free Memory Pointer
Engineering Team
Memory in der EVM
EVM-Memory ist ein linearer Byte-Array, der bei Adresse 0 beginnt und nach Bedarf waechst. Im Gegensatz zu Storage ist Memory fluechtig — er existiert nur waehrend einer einzelnen Transaktionsausfuehrung. Memory ist billig im Vergleich zu Storage, aber die quadratische Erweiterungskostenformel bestraft grosse Allokationen.
Die grundlegenden Memory-Opcodes
MSTORE und MLOAD
// 32 Bytes an Adresse 0x80 schreiben
mstore(0x80, 0x42)
// 32 Bytes von Adresse 0x80 lesen
let value := mload(0x80) // value = 0x42
MSTORE schreibt immer 32 Bytes. Wenn Sie nur 1 Byte schreiben moechten, verwenden Sie MSTORE8:
mstore8(0x80, 0xff) // Schreibt nur 1 Byte
CALLDATACOPY und RETURNDATACOPY
// Calldata in Memory kopieren
calldatacopy(0x80, 4, 32) // 32 Bytes ab Calldata-Offset 4 nach Memory 0x80
// Rueckgabedaten in Memory kopieren
returndatacopy(0x80, 0, returndatasize()) // Alle Rueckgabedaten nach 0x80
Der Free Memory Pointer
Solidity speichert einen Free Memory Pointer an Adresse 0x40. Dieser zeigt auf das naechste freie Byte im Memory:
// Free Memory Pointer lesen
let ptr := mload(0x40) // Anfangswert: 0x80
// Memory allozieren (64 Bytes)
let newPtr := add(ptr, 64)
mstore(0x40, newPtr) // Free Memory Pointer aktualisieren
// Jetzt koennen Sie ptr bis ptr+63 sicher verwenden
mstore(ptr, someValue)
mstore(add(ptr, 32), anotherValue)
Wichtig: Wenn Sie den Free Memory Pointer nicht aktualisieren, kann nachfolgender Solidity-Code Ihre Daten ueberschreiben. Wenn Sie vollstaendig in Yul arbeiten (kein Solidity-Wrapper), koennen Sie den Pointer ignorieren und das Memory frei verwalten.
Memory-Layout von Solidity
// Reservierte Bereiche:
// 0x00 - 0x3f: Scratch Space (64 Bytes) — temporaerer Arbeitsbereich fuer Hashing usw.
// 0x40 - 0x5f: Free Memory Pointer (32 Bytes)
// 0x60 - 0x7f: Zero Slot (32 Bytes) — immer Null
// 0x80 - ...: Freier Bereich — hier beginnen Allokationen
Der Scratch Space (0x00-0x3f) ist besonders nuetzlich: Sie koennen ihn fuer temporaere Berechnungen verwenden, ohne den Free Memory Pointer zu beruehren:
// keccak256 eines einzelnen Werts (kostenguenstig):
mstore(0x00, someValue)
let hash := keccak256(0x00, 32)
// Kein Pointer-Update noetig!
Manuelle ABI-Kodierung
In Yul muessen Sie Funktionsaufrufe manuell ABI-kodieren:
// ERC-20 transfer(address,uint256) aufrufen
let ptr := mload(0x40)
// Funktionsselektor: bytes4(keccak256("transfer(address,uint256)"))
mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
// Erstes Argument: Adresse (links auf 32 Bytes gepolstert)
mstore(add(ptr, 4), and(recipient, 0xffffffffffffffffffffffffffffffffffffffff))
// Zweites Argument: Betrag
mstore(add(ptr, 36), amount)
// Externen Aufruf durchfuehren
let success := call(
gas(), // Alles verbleibendes Gas weitergeben
tokenAddress, // Ziel-Contract
0, // Kein ETH-Wert
ptr, // Input-Daten Start
68, // Input-Daten Laenge (4 + 32 + 32)
0x00, // Output Start
32 // Output Laenge
)
Speichererweiterungskosten optimieren
Die quadratische Kostenerweiterung bedeutet, dass Sie Memory sparsam verwenden sollten:
// Kosten fuer Memory-Erweiterung:
// Bis 724 Bytes: ~linear (3 Gas pro Wort)
// 1 KB: ~99 Gas
// 10 KB: ~1.000 Gas
// 100 KB: ~100.000 Gas
// 1 MB: ~3.000.000 Gas
Best Practices:
- Memory wiederverwenden — Wenn Sie temporaere Puffer brauchen, verwenden Sie den Scratch Space oder denselben Memory-Bereich mehrmals.
- Nicht mehr allozieren als noetig — Lesen Sie nur die Bytes, die Sie brauchen.
- Grosse Rueckgabedaten vermeiden — Jedes Byte kostet Erweiterung.
Fortgeschrittene Muster
Mehrere externe Aufrufe mit gemeinsamem Puffer
// Einen Memory-Bereich fuer mehrere Aufrufe wiederverwenden
let buf := 0x80 // Fester Puffer statt Allokation
// Aufruf 1
mstore(buf, selector1)
mstore(add(buf, 4), arg1)
pop(staticcall(gas(), target1, buf, 36, buf, 32))
let result1 := mload(buf)
// Aufruf 2 — gleicher Puffer!
mstore(buf, selector2)
mstore(add(buf, 4), arg2)
pop(staticcall(gas(), target2, buf, 36, buf, 32))
let result2 := mload(buf)
Memory-effiziente Hashberechnung
// Zwei Werte hashen, ohne neuen Memory zu allozieren
mstore(0x00, value1)
mstore(0x20, value2)
let hash := keccak256(0x00, 64)
Fazit
Die Speicherverwaltung in Yul erfordert ein tiefes Verstaendnis des EVM-Memory-Layouts, der Erweiterungskosten und des Free Memory Pointers. Durch effiziente Nutzung des Scratch Space, Wiederverwendung von Puffern und minimale Allokationen koennen Sie den Gasverbrauch Ihrer Contracts erheblich reduzieren. Im naechsten Artikel werden wir diese Konzepte anwenden, um gaseffiziente Schleifen und Verzweigungen in Yul zu implementieren.