Techniques to cut gas costs for your dapps

feature-image

Play all audios:

Loading...

Gas costs play a significant role in the long run for each Ethereum-based Dapp. We share several hands-on techniques, by example, to help your Dapps cut the associated gas costs. We would


love to hear your feedback if any. TERMS we use in the article: * SC: Smart Contract * GC: Gas Cost * SOLC: Solidity Compiler * EVM: Ethereum Virtual Machine OVERALL APPROACH:


_Feedback-based solution._ We tried to optimize the design and implementation of our SC the way it is expected from SOLC and EVM. We verified the ideas by performing GC regression tests and


apply the feedback back to the design and implementation. TECHNIQUES we used to cut gas costs (_note that we use SOLC version 0.4.21 for discussion purpose)_: * _BIT-COMPACTION_


Bit-compaction format for external function parameters helps cut gas costs because it minimizes the amount of input data sent to the Ethereum blockchain. Please note that it introduces


certain small extra gas costs to unpack the data. However, the savings usually weigh out the extra costs. Take the following code snippet for example: _pragma solidity ^0.4.21;__contract 


bitCompaction {   function oldExam(uint64 a, uint64 b, uint64 c, uint64 d) public {   }   function newExam(uint256 packed) public {   } }_ Its corresponding assembly code shows that the


oldExam function incurs 4 `_CALLDATALOAD`_ operations once it is called, and each `CALLDATALOAD` operation triggers one memory allocation operation in Ethereum, while the newExam function


incurs only 1 instead. Note that the assembly code is too large for us to present here, but one can easily generate it by performing (for example) `solc — asm — optimize — optimize-runs 200


bitCompaction.sol`. In the experiment, we have the following call data for each function: oldExam call data: ([]uint8) (len=132 cap=132) { 00000000  3e f2 62 fd 00 00 00 00  00 00 00 00 00 


00 00 00 00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 00000020  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00 00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00


00000040  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00 00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 00000060  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00 00000070  00 


00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 00000080  00 00 00 01 }newExam call data: ([]uint8) (len=36 cap=36) { 00000000  83 ba 6e 5a 00 00 00 00  00 00 00 01 00 00 00 00 00000010  00 00


 00 01 00 00 00 00  00 00 00 01 00 00 00 00 00000020  00 00 00 01 } As we can see, those uint64 parameters in the oldExam function are first converted to uint256 ones once being called. Gas


costs for the above two are 22235 and 21816 respectively, meaning the newExam function saves 419 gas costs. The more scattered the parameters are, the more savings you cut by adopting the


bit compaction method. * _BATCHING_ Batching cuts gas cost because it reduces common data processing. Take the following code snippet for example: _Old: func once(uint256 header, uint256 


val...) x N New: func batch(uint256 header, uint256[] val... x N)_ Executing the old function N times processes the common `header` field N times, as well as calling the function N times.


However, by batching, the function is called only one time and the common `header` field is processed only one time. Therefore, it saves gas costs by reducing `CALLDATALOAD`, memory


allocation, and function call operations. The larger the N, the larger the savings by adopting the batching mechanism. * _SEPARATING WRITES TO STORAGE STRUCT_ Separating writes to struct


object that is defined in storage, cuts gas cost in many cases. Take the following code snippet for example: _pragma solidity ^0.4.21;__contract structWrite {   struct Object {     uint64 


v1;     uint64 v2;     uint64 v3;     uint64 v4;   }__  Object obj;__  function StructWrite() public {     obj.v1 = 1;     obj.v2 = 1;     obj.v3 = 1;     obj.v4 = 1;   }__  function 


oldExam(uint64 a, uint64 b) public {     uint a0; uint a1; uint a2; uint a3; uint a4; uint a5; uint a6;      uint b0; uint b1; uint b2; uint b3; uint b4; uint b5;__    obj.v1 = a + b;     


obj.v2 = a - b;     obj.v3 = a * b;     obj.v4 = a / (b + 1);   }__  function setObject(uint64 v1, uint64 v2, uint64 v3, uint64 v4) private {     obj.v1 = v1;     obj.v2 = v2;     obj.v3 = 


v3;     obj.v4 = v4;   }__  function newExam(uint64 a, uint64 b) public {     uint a0; uint a1; uint a2; uint a3; uint a4;     uint b0; uint b1; uint b2; uint b3;     setObject(a + b, a - b,


 a * b, a / (b + 1));   } }_ Once the optimize option is turned on, for the above example, SOLC compiles storage struct writes in such a way that the oldExam function would incur 1 EVM


`SSTORE` operation (`SSTORE` is the most gas costly operation) for each of the struct field in Object since the current implementation of SOLC could not optimize the `SSTORE` operations when


there is not enough stack space (there is stack space for only 16 local variables). However, with the new approach, it has ample stack space such that the current compiler can optimize the


struct accesses by performing, in total, only 1 `SSTORE` operation after merging writes to the struct. This can be easily verified against by looking at the corresponding assembly code. This


is restricted to the current implementation of SOLC, and is subject to change in the future. For the above example, the gas costs are 58140 and 27318 respectively, indicating a gas cost


saving of 30822 for the newExam function. When the stack space is not enough, the more fields the struct has, the more gas cut you achieve if the new approach is leveraged. * _UINT256 AND


DIRECT MEMORY ACCESS_ Calculation unit in SOLC is uint256. Hence other types (e.g., uint8) require type conversions before calculations are applied. This incurs extra gas costs. Moreover,


direct memory access is more GC-efficient than direct storage access, and more GC-efficient than struct pointer based access. These smaller tricks can be represented as: _uint8 data;


                  __=>  __ uint256 data;__uint256 val = storageData;   =>   uint256 memoryData = storageData; (N Times)                         uint256 val = memoryData;__uint64 val = 


obj.v1;         __=>  __ uint64 val = val1;_ * _ASSEMBLY OPTIMIZATION_ When compiling your SOLC code, make sure to perform GC experiments with the SOLC `optimize — runs` option to figure


out the best GC-efficient assembly code to run in EVM. CITATION @article{li2018gassaving,   title   = "Techniques to Cut Gas Costs for Your Dapps"",   author  = "Li, 


Zhichao",   journal = "medium.com",   year    = "2018",   month   = "June",   url     = 


"https://medium.com/coinmonks/techniques-to-cut-gas-costs-for-your-dapps-7e8628c56fc9" } Or: Li, Zhichao. (Jun 2018). "Techniques to Cut Gas Costs for Your Dapps". 


Medium.com. https://medium.com/coinmonks/techniques-to-cut-gas-costs-for-your-dapps-7e8628c56fc9. REFERENCE * Gas Costs from Yellow Paper. _LINK_.