Kad radimo revizije koda pametnih ugovora, u izvještaj uključujemo i tehnike i prijedloge za optimizaciju potrošnje gas-a koje mogu vaš ugovor učiniti dugoročno jeftinijim. To nije toliko bitno kod ICO-ova koji će se izvršiti jednom i nikad više, ali je ključno kod dapp-ova koji dugo žive, decentraliziranih burzi, ugovora koji su namijenjeni da ih koriste drugi ugovori, itd.

Evo nekih korisnih savjeta za optimizaciju gas-a.

Prvo, naučite što je gas ovdje – članak će pokriti što je, odakle dolazi, kako se plaća, koliko, zašto, itd.

Odstranite viška linije koda

Postoji toliko suvišnog koda u raznim ugovorima da jednostavno moramo reći sljedeće: prije pokretanja ugovora na blockchainu, provjerite imate li koje neiskorištene funkcije. To podrazumijeva i tuđi kod. Ako vaš dodani SafeMath ugovor služi da koristite samo uint256.add a ne i sub, mul i div, onda obrišite potonje iz ugovora. Ne treba vam cijela biblioteka funkcija u vašem ugovoru ako koristite samo njenih 5%.

Osim ako je funkcionalnost koju tražite vrlo kompleksna (u tom slučaju će ionako vjerojatno već biti negdje na blockchainu i moći ćete je ponovno iskoristiti), općenito se isplati kopirati samo one funkcije iz tuđih ugovora koje ćete zapravo i koristiti.

Redoslijed funkcija nije nebitan

Ako imate skuplju funkciju f() i jeftiniju funkciju g() i ako pozitivno izvršenje bilo koje od njih aktivira neki logički uvjet, pobrinite se da se ona jeftinija nalazi na prvom mjestu. Zašto? Pogledajmo ovu logičku tablicu.

ConditionOperand AOperand BResult
OR111
OR101
OR011
OR000

Ovo je standardna logička tablica za uniju skupova tj. “ili” uvjet. Ako je bilo koji od uvjeta (tj. ili a ili b) istinit, tada je cijeli logički sklop istinit. Većina programskih okruženja će prestati s evaluacijom sklopa kada naiđe na prvi pozitivni rezultat, jer u tom slučaju sklop je svejedno istinit pa nema potrebe za provjerom drugog. Time se g() preskače.

Dakle, ako je f skuplji od g, onda:

if (g() || f()) { ... }

ispada jeftinije nego:

if (f() || g()) { ... }

… ali samo ako f ima više šanse da se ispuni od g.

Razmišljajte o redoslijedu kada pišete ovakve sklopove i poredajte funkcije s troškom i vjerojatnošću na umu.

Izbjegavajte petlje

Generalno bi se petlje trebale izbjegavati. Da, EVM jest Turing-potpuno okruženje što znači da petlje u njemu mogu izvršiti kompleksne zadatke, no to ne znači da bi trebale.

Ako izbjegavanje petlji nije moguće, probajte izbjegavati beskonačne ili nedefinirane petlje, tj. petlje u kojima ne znate maksimalni broj ponavljanja. Ako program ne zna koliko ponavljanja očekivati, tada ne može ni predvidjeti koliko će približno gas-a biti potrebno za njegovo pokretanje, što šteti korisničkom doživljaju aplikacije.

Još gore, petlja bez granica lako potroši sav dostupni gas – što ako imamo tisuće korisnika u pametnom ugovoru i na svima želimo odraditi neku operaciju? Petlja bi mijenjala korisnike do kada ne bi ostala bez gasa, i u tom bi se momentu stanje vratilo na početno. Pokretač programa ostao bi bez gasa, a efekta ne bi bilo. Bolje je preuzeti korisnike na strani klijenta, podijeliti ih u setove fiksne veličine, i slati ih u pametni ugovor na obradu u ograničenim fiksnim setovima.

Memorija i dugoročno spremanje

Koristite storage (dugoročno spremanje) samo kad ste gotovi s računanjem. Kada neku varijablu koristite više puta u funkciji, čitajte je iz memorije.

Ovaj članak to dobro objašnjava:

SLOAD opcode koji čita podatke iz dogoročnog spremišta košta 200 gas, dok MSTORE i MLOAD koji pišu i čitaju u memoriju koštaju 3 gas svaki.

uint256 public num = 50;

function readStorage() public {
    if (num == 40 || num <= 10 && num > 100) {
        // execution cost: 885 gas
    }
}

function readMemory() public {
    uint256 mnum = num;
    if (mnum == 40 || mnum <= 10 && mnum > 100) {
        // execution cost: 605 gas
    }
}

Izgleda kao mala razlika, no uz mnogo poziva trošak će znatno narasti.

Veličina tipa podataka i arrayevi fiksnih veličina

Koristite arrayeve fiksne veličine kad god je to moguće (vidi dolje).

Koristite 32 bytes / 256 bits kad god je to moguće jer su to najoptimiziranije vrste spremanja podataka. Npr. kada koristite male brojke, spremanje u uint8 nije ništa efikasnije od uint256 za taj isti broj. Kao što ovaj članak pokazuje:

/*
    SSTORE opcode which writes a data word to storage costs 20000 gas + some amount depending on the type.
    SLOAD opcode which reads a data word from storage costs 200 gas + some amount depending on the type.

    Intuition would tell us gas optimizations can be acheived by using smaller data types.
    However this is only the case inside of structs.
    Large types should always be used unless struct packing is possible.
*/

uint256 public integer256;

function write() public {
    integer256 = 1;         // 20430 gas
}

function read() public returns (uint256) {
    return integer256;      // 656 gas
}


uint128 public integer128;

function write() public {
    integer128 = 1;         // 20648 gas
}

function read() public returns (uint128) {
    return integer128;      // 671 gas
}

Ovo je ujedno zašto SafeMath ugovor koristi samo uint256 i ni ne pokušava nadograditi druge tipove podataka.

Stringovi

Koristite bytes32 umjesto string za manje stringove poput korisničkih imena. Prema dokumentaciji, moguće je koristiti array bajtova (byte[]) ali to koristi previše prostora – 31 bajt za svaki element kada se šalje u pozivima na druge funkcije. Bolje je koristiti bytes. U pravilu, koristite bytes za nizove sirovih bajtova nepoznate duljine, dok se stringovi koriste za UTF–8 podatke nepoznate duljine (riječi). Ako možete ograničiti duljinu na neki broj bajtova (npr kod korisničkih imena), uvijek koristite jedan od bytes1 do bytes32 jer su mnogo jeftiniji od bytes.

Više o tome ovdje.

Struct Packing

Struct u Soliditiju je oblik podatka koji sadrži druge podatke. Npr:

struct Korisnik {
  uint256 id;
  string korisnicko_ime;
}

No structovi mogu komprimirati podatke i spremiti ih u “spakirani” oblik pod uvjetom da su podaci spremljeni u tipove koji su zbrojeni djeljivi sa 32 bajta, kao što je objašnjeno u ovom članku.

Primjer:

/*
    Structs allow to combine data types into single data words of 32 bytes.

    Things to keep in mind:
    1. Putting a 32 bytes type in a struct is more expensive than keeping it outside.
    2. Packing works in multiples of 32 bytes therefore putting a type smaller than 32 bytes alone in a struct does not cause any saving
    3. It's possible to pack numbers and strings together.
*/

struct Struct {
    uint256 num;
}
Struct public s;
function write() public {
    s = Struct(1);              // 20497 gas
}
function read() public returns (uint256) {
    return s.num;               // 656 gas
}

struct Struct {
    uint128 num1;
    uint128 num2;
}
Struct public s;
function write() public {
    s = Struct(1,2);            // 20775 gas (instead of 40000+)
}
function read() public returns (uint128,uint128) {
    return (s.num1,s.num2) ;    // 730 gas
}

struct Struct {
    uint128 num1;
    bytes16 byte1;
    uint128 num2;
    bytes16 byte2;
}
Struct public s;
function write() public {
    s = Struct(1,'2',3,'4');                 // 41044 gas (instead of 80000+)
}
function read() public returns (uint128,bytes16,uint128,bytes16) {
    return (s.num1,s.byte1,s.num1,s.byte2);  // 1014 gas
}

Ovo je malo teže predvidjeti i isplanirati, ali kao što vidite može rezultirati velikim uštedama.

Optimizacija

Kompajler za Solidity dolazi s ugrađenim optimizatorom koda koji “prožvače” kod i vraća optimiziranu verziju. To je donekle objašnjeno u dokumentaciji.

Pokreće se na sljedeći način:

solc --optimize --bin sourceFile.sol

Ili s više / manje runova (pokretanja) ovako:

solc --optimize --runs=250 --bin sourceFile.sol

Runs nisu koliko puta će se optimizator pokrenuti, već koliko puta očekujemo da će se ovaj kod koristiti.

Obično optimizator uzima za neku normu 200 puta. Ako želite optimizirati kod za prvo slanje na mainnet i stavljanje svog pametnog ugovora u pogon, odaberite runs=1 jer će to proizvesti najmanju datoteku i time koštati najmanje gas-a. Ako pak očekujete mnogo transakcija i nije vam bitno koliko je skupo prvi puta pokrenuti ugovor, odaberite visoki broj.

Povrat gas troškova

Kada se kompleksni i skupi pametni ugovor stavi na Ethereum mrežu, on zauzima mnogo mjesta. Oslobađanje tog mjesta sustav nagrađuje povratom dijela gas-a plaćenog za stavljanje tog ugovora online, ali ne konkretno u etheru nego u popustu na sljedeću transakciju koju pokrećete.

Na primjer, pretpostavimo da lansiranje ugovora A košta X gas-a, a A ima funkciju za samo-uništenje koju možemo pozvati. Pretpostavimo i da pokretanje ugovora B košta Y, a htjeli bismo da Y bude manji. U tom slučaju možemo učiniti sljedeće: destroy(A) && deploy(B), čime će trošak od deploy(B) biti Y – X/2 umjesto Y. Uništenje A pojeftinjuje pokretanje B.

Ovo je ujedno i metoda koju koristi Gastoken za tokeniziranje gas-a. Dozvoljava lansiranje lažnog velikog ugovora kada je gas jeftin (kada mreža nije zakrčena) da bi ga se uništilo u momentu kada je gas skuplji. Iako to nije baš tehnika za optimizaciju potrošnje, može biti veoma korisna kad treba poslati transakciju usred zakrčenosti mreže.

Assembly i cijene opcode-ova

Ne zaboravite da je moguće pisati i assembly kod u samom Soliditiju, pri čemu bi ovaj popis svih troškova za opcodes mogao biti korisan. Korištenjem ovog popisa stoga možete mikro-optimizirati vaš kod za potrošnju gas-a umjesto da se pouzdate na kompajler da odradi interpretaciju vašeg Solidity koda. Iako je assembly mnogo teži za pisanje, ovakav pristup može rezultirati daleko jeftinijim ugovorima.

Naučite pisati assembly ovdje.

Imate li vi koje savjete za optimizaciju gas potrošnje? Javite nam!

LEAVE A REPLY

Please enter your comment!
Please enter your name here