乐闻世界logo
搜索文章和话题

How to optimize Gas in Solidity smart contracts? What are common optimization techniques?

3月6日 21:51

Gas optimization is a key skill in Solidity development that directly affects contract execution costs and user experience. Here are systematic Gas optimization strategies and techniques.

1. Storage Optimization

Storage is the most expensive resource, and optimizing storage usage is the primary task of Gas optimization.

Using Appropriate Data Types

solidity
contract StorageOptimization { // Not recommended: using uint256 for small values uint256 public smallValue; // Occupies 32 bytes // Recommended: use smaller data types uint128 public value128; // Occupies 16 bytes uint64 public value64; // Occupies 8 bytes uint32 public value32; // Occupies 4 bytes uint8 public value8; // Occupies 1 byte // Best: pack storage variables uint128 public balance; // slot 0: 16 bytes uint32 public timestamp; // slot 0: 4 bytes uint16 public status; // slot 0: 2 bytes address public owner; // slot 0: 20 bytes - needs slot 1 // Note: uint128 + uint32 + uint16 = 22 bytes, address needs new slot }

Storage Variable Packing

solidity
contract PackingExample { // Unoptimized: occupies 3 storage slots struct Unoptimized { uint256 a; // slot 0 uint128 b; // slot 1 uint128 c; // slot 2 } // Optimized: only occupies 2 storage slots struct Optimized { uint128 b; // slot 0 (16 bytes) uint128 c; // slot 0 (16 bytes) - packed together uint256 a; // slot 1 } // Best practice: sort by size struct BestPractice { uint128 a; // 16 bytes uint128 b; // 16 bytes - shares slot with a uint64 c; // 8 bytes uint64 d; // 8 bytes - shares with c uint32 e; // 4 bytes uint32 f; // 4 bytes - shares with e } }

Using Memory Instead of Storage

solidity
contract MemoryVsStorage { uint256[] public data; // Expensive: multiple storage accesses function expensiveSum() public view returns (uint256) { uint256 sum = 0; for (uint i = 0; i < data.length; i++) { sum += data[i]; // Accesses storage each loop iteration } return sum; } // Optimized: load to memory first function optimizedSum() public view returns (uint256) { uint256[] memory localData = data; // Load once uint256 sum = 0; for (uint i = 0; i < localData.length; i++) { sum += localData[i]; // Accesses memory, much cheaper } return sum; } }

2. Variable and Calculation Optimization

Using Constants and Immutables

solidity
contract ConstantOptimization { // Expensive: storage variable uint256 public constantValue = 1000; // Optimized: constant (doesn't occupy storage) uint256 public constant CONSTANT_VALUE = 1000; // Better: immutable (determined at compile time, doesn't occupy storage) uint256 public immutable immutableValue; constructor(uint256 _value) { immutableValue = _value; } // Use constant for calculations function calculate(uint256 amount) public pure returns (uint256) { return amount * CONSTANT_VALUE / 10000; } }

Short-Circuit Evaluation

solidity
contract ShortCircuit { // Optimization: leverage short-circuit evaluation function checkConditions(bool a, bool b, bool c) public pure returns (bool) { // Put conditions most likely to be false first return a && b && c; } // Optimization: || operator function checkOr(bool a, bool b, bool c) public pure returns (bool) { // Put conditions most likely to be true first return a || b || c; } }

3. Loop Optimization

Loop Variable Optimization

solidity
contract LoopOptimization { uint256[] public data; // Unoptimized: accesses storage each loop iteration function unoptimizedLoop() public view returns (uint256) { uint256 sum = 0; for (uint256 i = 0; i < data.length; i++) { sum += data[i]; } return sum; } // Optimization 1: cache array length function optimizedLoop1() public view returns (uint256) { uint256 sum = 0; uint256 len = data.length; // Cache length for (uint256 i = 0; i < len; i++) { sum += data[i]; } return sum; } // Optimization 2: use unchecked and ++i function optimizedLoop2() public view returns (uint256) { uint256 sum = 0; uint256 len = data.length; for (uint256 i = 0; i < len; ) { sum += data[i]; unchecked { ++i; } // Use unchecked and pre-increment } return sum; } // Optimization 3: use memory array function optimizedLoop3() public view returns (uint256) { uint256[] memory localData = data; uint256 sum = 0; uint256 len = localData.length; for (uint256 i = 0; i < len; ) { sum += localData[i]; unchecked { ++i; } } return sum; } }

4. Function Optimization

Using Calldata

solidity
contract CalldataOptimization { // Expensive: using memory function processMemory(uint256[] memory data) external pure returns (uint256) { uint256 sum = 0; for (uint i = 0; i < data.length; i++) { sum += data[i]; } return sum; } // Optimized: use calldata (read-only, no copy) function processCalldata(uint256[] calldata data) external pure returns (uint256) { uint256 sum = 0; for (uint i = 0; i < data.length; i++) { sum += data[i]; } return sum; } }

Function Modifier Optimization

solidity
contract ModifierOptimization { // Expensive: modifier code inlined into each function modifier expensiveCheck() { require(msg.sender == owner, "Not owner"); require(!paused, "Paused"); require(balance > 0, "No balance"); _; } // Optimized: extract checks into internal function modifier optimizedCheck() { _checkAccess(); _; } function _checkAccess() internal view { require(msg.sender == owner, "Not owner"); require(!paused, "Paused"); require(balance > 0, "No balance"); } // Compiler can optimize more effectively this way function action1() external optimizedCheck { } function action2() external optimizedCheck { } function action3() external optimizedCheck { } }

5. Error Handling Optimization

Custom Errors (Solidity 0.8.4+)

solidity
// File: ErrorOptimization.sol // License: MIT pragma solidity ^0.8.4; contract ErrorOptimization { address public owner; uint256 public balance; // Define custom errors (more Gas efficient than require strings) error NotOwner(address caller); error InsufficientBalance(uint256 requested, uint256 available); error InvalidAmount(uint256 amount); modifier onlyOwner() { if (msg.sender != owner) { revert NotOwner(msg.sender); } _; } function withdraw(uint256 amount) external onlyOwner { if (amount == 0) { revert InvalidAmount(amount); } if (amount > balance) { revert InsufficientBalance(amount, balance); } balance -= amount; payable(msg.sender).transfer(amount); } }

6. Event and Log Optimization

solidity
contract EventOptimization { // Expensive: storing large amounts of data mapping(uint256 => Transaction) public transactions; struct Transaction { address from; address to; uint256 amount; uint256 timestamp; string metadata; } // Optimized: use events instead of storage event TransactionExecuted( address indexed from, address indexed to, uint256 amount, uint256 timestamp ); function executeTransaction(address to, uint256 amount) external { // Execute business logic... // Use event to record (much cheaper than storage) emit TransactionExecuted(msg.sender, to, amount, block.timestamp); } }

7. Library Usage

solidity
// Use libraries to reduce code duplication library SafeMath { function add(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { uint256 c = a + b; require(c >= a, "Addition overflow"); return c; } } } contract UsingLibrary { using SafeMath for uint256; function calculate(uint256 a, uint256 b) public pure returns (uint256) { return a.add(b); // Use library function } }

8. Bitwise Operation Optimization

solidity
contract BitwiseOptimization { // Use bitwise operations instead of mathematical operations // Multiply/divide by powers of 2 function multiplyBy2(uint256 x) public pure returns (uint256) { return x << 1; // Equivalent to x * 2 } function divideBy2(uint256 x) public pure returns (uint256) { return x >> 1; // Equivalent to x / 2 } // Check if power of 2 function isPowerOf2(uint256 x) public pure returns (bool) { return x > 0 && (x & (x - 1)) == 0; } // Use bit masks to store multiple boolean values uint256 private flags; uint256 constant FLAG_PAUSED = 1 << 0; // 1 uint256 constant FLAG_FINALIZED = 1 << 1; // 2 uint256 constant FLAG_APPROVED = 1 << 2; // 4 function setFlag(uint256 flag) internal { flags |= flag; } function clearFlag(uint256 flag) internal { flags &= ~flag; } function hasFlag(uint256 flag) internal view returns (bool) { return (flags & flag) != 0; } }

9. Compiler Optimization

Using Optimizer

json
// hardhat.config.js module.exports = { solidity: { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 200 // Adjust based on contract call frequency } } } };

10. Gas Optimization Comparison Table

Optimization TechniqueGas SavingsApplicable Scenarios
Storage packing20,000 Gas/slotMultiple small variables
Use constants20,000 GasFixed values
Use calldata3,800 Gas/32 bytesExternal function parameters
Custom errors~50 GasError handling
unchecked~80 Gas/operationMathematical operations
Events replace storage~19,000 GasLogging
Short-circuit evaluationVariableConditional judgments

11. Optimization Tools

  1. hardhat-gas-reporter: Reports Gas consumption for each test
  2. eth-gas-reporter: Detailed Gas analysis reports
  3. Remix IDE: Built-in Gas estimation
  4. Tenderly: Gas analysis and optimization suggestions
javascript
// hardhat.config.js require("hardhat-gas-reporter"); module.exports = { gasReporter: { enabled: true, currency: "USD", gasPrice: 21 } };

12. Best Practices Summary

  1. Prioritize storage optimization: Storage operations are the most expensive
  2. Use appropriate data types: Avoid over-allocation
  3. Leverage compiler optimization: Enable optimizer and adjust runs parameter
  4. Batch operations: Reduce function call frequency
  5. Use events: Replace unnecessary storage writes
  6. Lazy computation: Compute when needed rather than storing
  7. Regular audits: Use tools to check Gas consumption

Remember: Over-optimization may reduce code readability. Find a balance between performance and maintainability.

标签:Solidity