Solidity Gas 最佳化實踐完整指南:2026 年最新技術

Gas 最佳化是以太坊智能合約開發中至關重要的課題,直接影響合約的部署成本和用戶的交易費用。隨著以太坊網路的發展和各類 Layer 2 解決方案的成熟,Gas 最佳化的策略也在持續演進。2025-2026 年期間,EIP-7702 的實施、Proto-Danksharding 帶來的 Blob 資料成本降低、以及各類新型最佳化技術的出現,都為 Gas 最佳化帶來了新的維度。本指南將從工程師視角深入

Solidity Gas 最佳化實踐完整指南:2026 年最新技術

概述

Gas 最佳化是以太坊智能合約開發中至關重要的課題,直接影響合約的部署成本和用戶的交易費用。隨著以太坊網路的發展和各類 Layer 2 解決方案的成熟,Gas 最佳化的策略也在持續演進。2025-2026 年期間,EIP-7702 的實施、Proto-Danksharding 帶來的 Blob 資料成本降低、以及各類新型最佳化技術的出現,都為 Gas 最佳化帶來了新的維度。本指南將從工程師視角深入探討 Solidity Gas 最佳化的各個層面,包括 Gas 消耗機制、常見最佳化模式、2026 年最新技術、以及實際的程式碼範例,幫助開發者編寫更加高效的智能合約。

一、Gas 機制與消耗原理

1.1 以太坊 Gas 基礎

理解 Gas 最佳化首先需要深入理解以太坊的 Gas 消耗機制。每一個 EVM 操作都有一個固定的 Gas 成本,這些成本反映了該操作對網路資源的消耗程度。

基礎操作 Gas 成本

操作類型Gas 成本說明
SLOAD2100讀取儲存槽
SSTORE (cold)22100首次寫入儲存槽
SSTORE (dirty)2900更新現有儲存槽
CALL2600基本調用成本
CREATE32000合約創建
LOG 操作取決於資料量事件日誌

資料儲存成本

Gas 成本公式
─────────────────────────────────────────────────
│
│  零位元組 (zero byte)
│  gas = 4
│
│  非零位元組 (non-zero byte)
│  gas = 16 (在 Calldata 中)
│  gas = 68 (在記憶體中)
│
│  儲存槽操作
│  gas = 20000 (當值從 0 變為非 0)
│  gas = 5000 (當值從非 0 變為非 0)
│  gas = 0 (當值從非 0 變為 0,但會退還 Gas)
│
└─────────────────────────────────────────────────

1.2 EIP-1559 費用機制

2025-2026 年,以太坊仍繼續使用 EIP-1559 費用機制:

EIP-1559 費用結構
─────────────────────────────────────────────────
│
│  基本費用 (Base Fee)
│  ├── 由網路自動調整
│  ├── 每區塊目標 15M Gas
│  ├── 範圍:最小 8 Gwei 到 最大可變
│  └── 費用會被燃燒
│
│  優先費用 (Priority Fee / Tip)
│  ├── 由用戶設定
│  ├── 給驗證者的小費
│  └── 通常 1-10 Gwei
│
│  最大費用 (Max Fee)
│  ├── 用戶願意支付的最高費用
│  ├── = Base Fee + Priority Fee
│  └── 超過部分退還
│
└─────────────────────────────────────────────────

1.3 Layer 2 的 Gas 考量

隨著 Layer 2 的普及,Gas 最佳化也需要考慮 L2 的特性:

各 Layer 2 的 Gas 特性

Layer 2資料可用性L1 費用份額備註
ArbitrumRollup較低使用壓縮
OptimismRollup較低Bedrock 升級
BaseRollup較低基於 OP Stack
zkSync ErazkRollup中等L2 原生費用
StarknetzkRollup中等不同收費模式

Blob 費用(EIP-4844 影響)

EIP-4844 引入的 Blob 為 L2 資料可用性提供了更便宜的方式:

Blob 費用計算
─────────────────────────────────────────────────
│
│  每個 Blob = 4096 field elements = 128 KB
│
│  Blob 費用 = Base Fee × Blob Gas Price × Blob 數量
│
│  費用波動
│  ├── 由 Target 與 Max 比例決定
│  ├── Target: 3 Blob/區塊
│  └── Max: 6 Blob/區塊
│
│  對 L2 的影響
│  ├── 資料費用降低 10-100 倍
│  └── L2 交易成本大幅下降
│
└─────────────────────────────────────────────────

二、程式碼層級最佳化

2.1 變數排列最佳化

Solidity 編譯器會自動將變數打包成 32 位元組的儲存槽。透過精心設計變數順序,可以減少所需的儲存槽數量:

最佳化前

// 未最佳化 - 使用 5 個儲存槽
contract Unoptimized {
    uint256 public a;    // slot 0
    uint128 public b;    // slot 1
    uint128 public c;    // slot 2
    uint256 public d;    // slot 3
    address public e;    // slot 4
}

最佳化後

// 最佳化 - 只使用 3 個儲存槽
contract Optimized {
    uint256 public a;    // slot 0
    uint128 public b;    // slot 1 (與 c 打包)
    uint128 public c;    // slot 1
    uint256 public d;    // slot 2 (與 e 打包)
    address public e;    // slot 2
}

變數打包規則

2.2 函數可見性最佳化

函數的可見性會影響 Gas 消耗:

// 最佳化原則
contract VisibilityOptimization {

    // 1. 盡可能使用 internal
    // internal 函數會被內聯,省去調用成本
    function _calculate(uint256 x) internal pure returns (uint256) {
        return x * 2;
    }

    // 2. 避免 public 在複雜合約中
    // public 函數會檢查 msg.sender 並有額外開銷
    // 使用 external 替代 public 當不需要內部調用時
    function execute(uint256 amount) external pure returns (uint256) {
        return _calculate(amount);
    }

    // 3. 使用 view 修飾符避免狀態修改
    function getValue() external view returns (uint256) {
        return storedValue;
    }
}

2.3 迴圈最佳化

迴圈是 Gas 消耗的常見來源:

最佳化前

// 未最佳化迴圈
function sumUnoptimized(uint256[] storage arr) external view returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < arr.length; i++) {
        total += arr[i];
    }
    return total;
}

最佳化後

// 最佳化迴圈
function sumOptimized(uint256[] storage arr) external view returns (uint256) {
    uint256 total = 0;
    uint256 length = arr.length;

    // 緩存 length,避免每次迴圈都讀取
    for (uint256 i = 0; i < length; ) {
        total += arr[i];
        unchecked {
            ++i;
        }
    }
    return total;
}

// 現代 Solidity 更簡潔的方式
function sumModern(uint256[] storage arr) external view returns (uint256 total) {
    assembly {
        mstore(0x00, arr.slot)
        let start := keccak256(0x00, 0x20)
        let length := sload(arr.slot)

        for { let i := 0 } lt(i, length) { i := add(i, 1) } {
            total := add(total, sload(add(start, i)))
        }
    }
}

2.4 映射與陣列最佳化

映射(Mapping)

映射的存取具有固定 Gas 成本:

contract MappingOptimization {

    // 標準映射
    mapping(address => uint256) public balances;

    // 最佳化:使用 mapping 避免迴圈
    function getBalance(address user) external view returns (uint256) {
        return balances[user];  // O(1) 存取
    }

    // 錯誤示範:使用陣列搜尋
    // 永遠不要在合約中這樣做
    // function findUser(address user) external view returns (uint256) {
    //     for (uint256 i = 0; i < users.length; i++) {
    //         if (users[i] == user) return i;
    //     }
    // }
}

陣列操作

// 最佳化陣列操作
contract ArrayOptimization {

    // 防止陣列無限增長
    uint256[] public data;
    uint256 public constant MAX_LENGTH = 1000;

    function add(uint256 value) external {
        require(data.length < MAX_LENGTH, "Max length reached");
        data.push(value);
    }

    // 使用 last-in-first-out 刪除避免空白
    function removeLast() external {
        require(data.length > 0, "Empty");
        data.pop();
    }

    // 如果需要按索引刪除,與最後一個交換
    function removeByIndex(uint256 index) external {
        require(index < data.length, "Invalid index");
        data[index] = data[data.length - 1];
        data.pop();
    }
}

三、儲存最佳化技術

3.1 緊湊儲存

位元打包

可以使用位元運算將多個小值存儲在一個儲存槽中:

// 位元打包示例
contract BitPacking {

    // 每個狀態欄位分配位元數
    // status: 2 bits (0-3)
    // level: 6 bits (0-63)
    // score: 24 bits (0-16777215)

    // 使用一個 uint256 存儲多個值
    uint256 public packedData;

    function setData(uint8 status, uint8 level, uint32 score) external {
        // 每個值左移到正確位置後進行 OR 運算
        uint256 packed = 0;
        packed |= uint256(status) << 0;    // bits 0-1
        packed |= uint256(level) << 2;     // bits 2-7
        packed |= uint256(score) << 8;    // bits 8-31

        packedData = packed;
    }

    function getStatus() external view returns (uint8) {
        return uint8(packedData & 0x03);
    }

    function getLevel() external view returns (uint8) {
        return uint8((packedData >> 2) & 0x3F);
    }

    function getScore() external view returns (uint32) {
        return uint32((packedData >> 8) & 0xFFFFFF);
    }
}

3.2 儲存收縮

儲存收縮技術

對於歷史資料,可以將舊資料移到歸檔儲存:

contract StorageCompaction {

    struct Record {
        uint256 value;
        uint256 timestamp;
        bool active;
    }

    // 活躍記錄存在主要儲存
    mapping(uint256 => Record) public activeRecords;

    // 歷史記錄存在較便宜的位置
    mapping(uint256 => mapping(uint256 => Record)) private historicalRecords;
    mapping(uint256 => uint256) private historyCount;

    function closeRecord(uint256 id) external {
        Record memory record = activeRecords[id];
        require(record.active, "Not active");

        // 移到歷史儲存
        uint256 count = historyCount[id];
        historicalRecords[id][count] = Record({
            value: record.value,
            timestamp: record.timestamp,
            active: false
        });
        historyCount[id] = count + 1;

        // 清除主要儲存
        delete activeRecords[id];
    }
}

3.3 記憶體 vs 儲存

理解記憶體和儲存的成本差異:

contract MemoryVsStorage {

    uint256[] public arr;

    function processWithMemory() external view returns (uint256) {
        // 在記憶體中創建拷貝
        uint256[] memory memArr = arr;

        // 記憶體操作便宜
        uint256 sum = 0;
        for (uint256 i = 0; i < memArr.length; i++) {
            sum += memArr[i];
        }
        return sum;
    }

    // 如果只需要讀取,不要寫入儲存
    function readOnly() external view returns (uint256) {
        // 只讀取,不修改
        return arr.length;
    }
}

四、呼叫最佳化

4.1 低層級呼叫

CALL vs DELEGATECALL

contract CallOptimization {

    // 使用 assembly 進行最佳化呼叫
    function callOptimized(
        address target,
        bytes memory data
    ) internal returns (bool, bytes memory) {
        assembly {
            let result := call(
                gas(),           // 傳遞剩餘 gas
                target,
                0,              // 轉移價值
                add(data, 0x20), // 資料指標
                mload(data),     // 資料長度
                0,              // 輸出指標
                0               // 輸出長度
            )
            let size := returndatasize()
            let ptr := mload(0x40)
            returndatacopy(ptr, 0, size)
            return(ptr, size)
        }
    }

    // 避免不必要的返回值
    function callWithoutReturn(address target, bytes memory data) internal {
        assembly {
            call(gas(), target, 0, add(data, 0x20), mload(data), 0, 0)
        }
    }
}

4.2 多重呼叫

批量處理多個調用以分散 Gas 成本

contract Multicall {

    function multicall(
        bytes[] calldata calls
    ) external returns (bytes[] memory results) {
        results = new bytes[](calls.length);

        for (uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory result) = address(this).delegatecall(
                calls[i]
            );
            require(success, "Call failed");
            results[i] = result;
        }
    }

    // 聚合多個 ERC20 餘額查詢
    function multicallBalanceOf(
        address token,
        address[] calldata accounts
    ) external view returns (uint256[] memory) {
        IERC20 erc20 = IERC20(token);
        uint256[] memory balances = new uint256[](accounts.length);

        for (uint256 i = 0; i < accounts.length; i++) {
            balances[i] = erc20.balanceOf(accounts[i]);
        }

        return balances;
    }
}

4.3 Reentrancy Guard 最佳化

// 客製化 Reentrancy Guard 以最佳化 Gas
contract OptimizedReentrancyGuard {

    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status != ENTERED, "ReentrancyGuard: reentrant call");

        // 在 assembly 中設置以節省 Gas
        assembly {
            _status := ENTERED
        }

        _;

        assembly {
            _status := NOT_ENTERED
        }
    }
}

五、2026 年最新最佳化技術

5.1 EIP-7702 最佳化

EIP-7702 為 EOA 帳戶臨時添加合約功能,這帶來了新的最佳化機會:

// EIP-7702 最佳化示例
contract EIP7702Optimization {

    // 為 EOA 添加批量授權功能
    function setBatchAllowance(
        address[] calldata tokens,
        address[] calldata spenders,
        uint256[] calldata amounts
    ) external {
        require(
            tokens.length == spenders.length &&
            tokens.length == amounts.length,
            "Length mismatch"
        );

        for (uint256 i = 0; i < tokens.length; i++) {
            IERC20(tokens[i]).approve(spenders[i], amounts[i]);
        }
    }

    // 臨時合約可以在調用時動態創建
    // 這允許更靈活的 Gas 最佳化策略
}

5.2 Yul/Assembly 最佳化

對於 Gas критические 的操作,可以使用 Yul 進行最佳化:

// Yul 最佳化示例:高效計算
contract YulOptimization {

    // 使用 assembly 最佳化幂運算
    function fastPower(uint256 base, uint256 exp) external pure returns (uint256 result) {
        assembly {
            // 快速幂算法
            result := 1
            for { } exp { } {
                if and(exp, 1) { result := mul(result, base) }
                base := mul(base, base)
                exp := shr(1, exp)
            }
        }
    }

    // 最佳化 keccak256
    function fastHash(bytes memory data) external pure returns (bytes32) {
        assembly {
            let result := keccak256(add(data, 0x20), mload(data))
            mstore(0x00, result)
            return(0x00, 0x20)
        }
    }

    // 批量記憶體分配
    function batchMstore(bytes32[] memory values) external pure {
        assembly {
            let ptr := mload(0x40)
            for { let i := 0 } lt(i, values.length) { i := add(i, 1) } {
                mstore(add(ptr, mul(i, 0x20)), mload(add(values, mul(add(i, 1), 0x20))))
            }
            mstore(0x40, add(ptr, mul(values.length, 0x20)))
        }
    }
}

5.3 鏈上資料壓縮

// 鏈上資料壓縮示例
contract // 使用簡 DataCompression {

   單的壓縮演算法存儲歷史價格
    // 假設價格變化在一定範圍內

    struct PriceData {
        uint32 timestamp;
        int16 priceChange;  // 壓縮的價格變化
    }

    PriceData[] public priceHistory;
    int256 public basePrice;

    function addPrice(int256 newPrice) external {
        int256 change = newPrice - basePrice;
        require(change >= type(int16).min && change <= type(int16).max, "Change too large");

        priceHistory.push(PriceData({
            timestamp: uint32(block.timestamp),
            priceChange: int16(change)
        }));

        basePrice = newPrice;
    }

    function getPrice(uint256 index) external view returns (uint256) {
        require(index < priceHistory.length, "Invalid index");

        int256 totalChange = basePrice;
        for (uint256 i = priceHistory.length - 1; i > index; i--) {
            totalChange -= int256(priceHistory[i].priceChange);
        }

        return uint256(totalChange);
    }
}

六、Gas 最佳化工具與測試

6.1 Hardhat-Gas-Reporter

// hardhat.config.js 配置
module.exports = {
  gasReporter: {
    enabled: true,
    currency: 'USD',
    coinmarketcap: 'YOUR_API_KEY',
    token: 'ETH',
    gasPriceApi: 'https://api.etherscan.io/api?module=proxy&action=eth_gasPrice',
    showTimeConsistency: true,
    showUnnamedContracts: false,
  },
};

6.2 Foundry 測試

// 使用 Foundry 進行 Gas 測試
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";

contract GasTest is Test {

    function testGasUsage() public view {
        // 測試不同函數的 Gas 消耗
        uint256 gasStart = gasleft();
        // 執行要測試的函數
        uint256 gasUsed = gasStart - gasleft();

        console.log("Gas used:", gasUsed);
    }

    function testAssertGas() public {
        // 斷言 Gas 使用在預期範圍內
        assertLt(gasleft(), 100000);
    }
}

6.3 程式碼分析工具

工具功能
Slither靜態分析、Gas 優化建議
Mythril符號執行、安全分析
SolhintLinting、程式碼規範
Tenderly交易模擬、Gas 分析

七、實際最佳化案例

7.1 ERC-20 最佳化

// 最佳化版 ERC-20
contract OptimizedERC20 {

    // 使用 compact 變數排列
    uint8 public decimals;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    // 事件使用 indexed 參數減少 Gas
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // 最佳化 transfer
    function transfer(address to, uint256 amount) external returns (bool) {
        require(to != address(0), "ERC20: transfer to zero address");

        uint256 fromBalance = balanceOf[msg.sender];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");

        // 使用 unchecked 最佳化
        unchecked {
            balanceOf[msg.sender] = fromBalance - amount;
        }
        balanceOf[to] += amount;

        emit Transfer(msg.sender, to, amount);
        return true;
    }

    // 最佳化 transferFrom
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool) {
        require(to != address(0), "ERC20: transfer to zero address");

        uint256 fromBalance = balanceOf[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");

        uint256 currentAllowance = allowance[from][msg.sender];
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
            unchecked {
                allowance[from][msg.sender] = currentAllowance - amount;
            }
        }

        unchecked {
            balanceOf[from] = fromBalance - amount;
        }
        balanceOf[to] += amount;

        emit Transfer(from, to, amount);
        return true;
    }
}

7.2 治理合約最佳化

// 最佳化版 Governor 合約
contract OptimizedGovernor {

    // 使用 bitmap 存儲投票狀態
    mapping(address => uint256) public voteBitmaps;

    function castVote(uint256 proposalId, uint8 support) external {
        uint256 bitmap = voteBitcasts[msg.sender];
        uint256 position = proposalId % 256;

        // 使用 bit 操作而不是 mapping
        if (support == 1) {
            voteBitmaps[msg.sender] = bitmap | (1 << position);
        } else if (support == 0) {
            voteBitmaps[msg.sender] = bitmap & ~(1 << position);
        }
    }

    function hasVoted(address voter, uint256 proposalId) external view returns (bool) {
        uint256 bitmap = voteBitmaps[voter];
        uint256 position = proposalId % 256;
        return (bitmap & (1 << position)) != 0;
    }
}

八、總結與建議

Gas 最佳化是一個需要持續關注的領域,隨著以太坊網路的發展和 EIP 的更新,最佳化策略也會不斷演進。以下是本指南的核心要點:

8.1 最佳化原則

  1. 先測量後優化:使用 Gas 分析工具識別瓶頸
  2. 權衡取捨:有時可讀性和安全性比 Gas 更加重要
  3. 保持簡單:過度最佳化可能增加複雜性和錯誤風險
  4. 關注 Layer 2:隨著 L2 普及,應考慮跨 Layer 的最佳化策略

8.2 2026 年重點關注

8.3 實踐建議

透過持續的最佳化實踐,可以顯著降低合約的部署和運行成本,提升用戶體驗,並在競爭激烈的區塊鏈應用市場中佔得先機。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!