以太坊 EVM Opcodes 完整參考手冊:Gas 消耗數學推導與實戰最佳化指南
本文深入剖析 EVM 完整 opcode 指令集,從基礎的算術運算到複雜的存儲操作,提供完整的 Gas 消耗數學推導與實戰最佳化指南。涵蓋記憶體成本二次函數推導、SSTORE 狀態機制、CALL 系列的 cold/warm access 定價模型、日誌操作的 Gas 計算、以及 EOF 時代新 opcode 的完整解析。提供大量 Solidity 和 Assembly 程式碼範例,幫助開發者編寫更省 Gas 的智能合約。
以太坊 EVM Opcodes 完整參考手冊:Gas 消耗數學推導與實戰最佳化指南
開場廢話
你知道 EVM 裡面最貴的那個指令是什麼嗎?不是乘法,不是除法,是個冷門到不行傢伙——CREATE2。就這麼一個指令,光基礎成本就要 32000 gas,比你發個普通轉帳貴了快一倍半。
我當年初學 Solidity 的時候,壓根兒沒想過要搞懂這些 opcode。覺得反正 IDE 會幫我算 gas,我管那麼多幹嘛?後來寫 DeFi 合約的時候被現實狠狠打臉——同一個功能,我寫的版本比人家貴三倍 gas,直接導致用戶不願意用。痛定思痛之下,我開始折騰 EVM opcode 底層,這才發現這片世界比想像中精彩得多。
所以這篇文章我想跟你聊聊 EVM opcode 那些事兒。不是那種官方文檔的抄襲版本,而是我實際踩坑踩出來的經驗,順便把 gas 消耗的數學推導都給你證明清楚。準備好了嗎?我們開始吧。
第一章:EVM Opcodes 基礎觀念重建
1.1 什麼是 Opcode?為什麼要了解它?
Opcode,全名是 Operation Code,就是 EVM 能聽懂的「指令」。你寫的 Solidity 代碼,最後編譯完畢就是一堆 opcode 的組合。EVM 讀到這些 opcode,就知道要幹什麼事了。
聰明如你可能會問:那我寫 Solidity 就好了,幹嘛要懂 opcode?
好問題。答案很殘酷:懂 opcode 的工程師寫出來的合約,就是比不懂的省 gas。就像懂組語的 C 程式設計師,能寫出比不懂組語的同事更高效的代碼一個道理。
更關鍵的是,有些安全漏洞只在 opcode 層面才看得清楚。比如說重入攻擊,如果你知道 CALL opcode 會把控制權交出去,那攻擊者就能在你的合約還沒更新餘額之前再次調用提款函數。純看 Solidity 語法?很多時候根本察覺不到這個問題。
1.2 EVM 的堆疊架構
EVM 是個堆疊機(Stack Machine),這點必須先搞清楚。什麼意思呢?就是所有的運算都是在堆疊上進行的,不像 x86 架構那樣有暫存器。
堆疊的最大深度是 1024 層,每一層能存 256 位元的資料。256 位元啊朋友們,比你家 CPU 的 64 位元寬了整整四倍。這設計不是沒道理的——以太坊大量用到密碼學運算,Keccak-256 輸出就是 256 位元,secp256k1 橢圓曲線運算也是 256 位元。統一寬度省去了很多轉換的功夫。
來張圖給你感性認知:
堆疊示意(Push 操作後):
[15] ← SP (Stack Pointer)
[14]
[13]
...
[2]
[1]
[0] ← 底部
Push 3 後:
[15] ← SP 移動
[14]
[13]
...
[2]
[1]
[0] ← 值 3 被放到這裡
執行的時候很直觀:PUSH1 03 PUSH1 05 ADD,EVM 會把 3 推入堆疊,再把 5 推入堆疊,然後 ADD 把最上面兩個值彈出、相加、再把結果推回去。
1.3 Opcode 編碼方式
EVM opcode 使用一至兩個位元組編碼。第一個位元組是 opcode 本身,範圍是 0x00 到 0xff。如果 opcode 需要額外參數,參數會緊跟在 opcode 後面。
常見的 PUSH 系列就是一個好例子:
| Opcode | 名稱 | 說明 |
|---|---|---|
| 0x60 | PUSH1 | 推入下一個位元組 |
| 0x61 | PUSH2 | 推入下兩個位元組 |
| ... | ... | ... |
| 0x7f | PUSH32 | 推入下 32 個位元組 |
所以當你寫 PUSH1 0x05 的時候,實際的位元組流是 0x60 0x05。0x60 告訴 EVM「我要推入一個位元組」,0x05 就是具體的值。
DUP 和 SWAP 系列也是固定一至兩個位元組編碼:
0x80 = DUP1
0x81 = DUP2
...
0x8f = DUP16
0x90 = SWAP1
0x91 = SWAP2
...
0x9f = SWAP16
LOG 系列稍微特別一點:
0xa0 = LOG0 (不帶 topic)
0xa1 = LOG1 (一個 topic)
0xa2 = LOG2
0xa3 = LOG3
0xa4 = LOG4 (最多四個 topic)
第二章:Gas 消耗的數學推導
2.1 Gas 的本質是什麼?
在聊具體數字之前,我想先跟你探討一下:Gas 為什麼是這個價格?
Gas 不是費用,是「計算工作量」的度量單位。每一個 opcode 都有固定的 gas 消耗,這個消耗反映了執行該 opcode 對網路造成的成本。
成本來自哪裡?主要有三塊:
- 計算成本:CPU 執行運算的時間
- 記憶體成本:RAM 的讀寫
- 存儲成本:區塊鏈狀態的讀寫
存儲最貴,這點必須記住。區塊鏈的狀態是所有節點都要同步保存的。你寫入一個位元組,全世界可能幾千個節點都要跟著改。這種「分佈式寫入」的成本當然高得離譜。
2.2 基礎 Gas 消耗表(完整版)
讓我給你整理一份完整的 opcode gas 消耗表,這可是我實際查閱黃皮書和客戶端源碼總結出來的:
停止與算術運算
| Opcode | 名稱 | Gas | 說明 |
|---|---|---|---|
| 0x00 | STOP | 0 | 啥都不做 |
| 0x01 | ADD | 3 | 加法 |
| 0x02 | MUL | 5 | 乘法 |
| 0x03 | SUB | 3 | 減法 |
| 0x04 | DIV | 5 | 整數除法 |
| 0x05 | SDIV | 5 | 有符號整數除法 |
| 0x06 | MOD | 5 | 取模 |
| 0x07 | SMOD | 5 | 有符號取模 |
| 0x08 | ADDMOD | 8 | 先加後模 |
| 0x09 | MULMOD | 8 | 先乘後模 |
| 0x0a | EXP | 10 + 10*log256(exponent) | 指數運算 |
| 0x0b | SIGNEXTEND | 5 | 符號擴展 |
看到 EXP 了嗎?這傢伙的 gas 消耗不是固定的,跟指數的大小有關。推導公式是:
C_EXP(exponent) = 10 + 10 * log256(exponent)
其中 log256 表示「需要多少個位元組來表示這個數」
實際實現:
C_EXP(exponent) = 10 + 10 * (1 + floor(log2(exponent) / 8))
如果 exponent = 0,返回 1,log2(0) = 0,所以 log256(0) = 0
C_EXP(0) = 10 + 10 * 0 = 10
這就是為什麼做 a ** b 的時候,指數越大 gas 越高。實際上 EVM 要做 b 次乘法迴圈(或者優化過的版本做 log(b) 次平方-乘法)。
2.3 雜湊運算的 Gas 推導
| Opcode | 名稱 | Gas | 說明 |
|---|---|---|---|
| 0x20 | KECCAK256 | 30 + 6 * words | Keccak-256 雜湊 |
KECCAK256 的 gas 消耗公式是:
C_KECCAK256 = 30 + 6 * ceil(length / 32)
其中 length 是輸入資料的位元組數
words = ceil(length / 32) = 所需的 32 位元組「字組」數量
推導過程:
設輸入長度為 L 位元組。KECCAK-256 使用 sponge 結構,複雜度與處理的位元組數成正比。
- 基本成本:30 gas
- 每 32 位元組額外:6 gas
- 所以如果 L = 64 位元組:30 + 6 * 2 = 42 gas
實際上現代 EVM(EIP-1884 後)對 KECCAK256 的成本做了調整:
C_KECCAK256 = 20 + 6 * words (EIP-1884 之後)
為什麼要調整?因為在 EIP-1884 之前,KECCAK256 的成本跟 SLOAD 一樣都是 800。這不合理——KECCAK256 是 CPU 密集運算,SLOAD 是 IO 密集運算,兩者成本不該相同。EIP-1884 把 KECCAK256 降到 30(後來又微調到 20),SLOAD 則從 800 升到 2100。
2.4 記憶體操作的 Gas 模型
記憶體可能是 EVM 裡最複雜的定價系統。讓我一步一步推導給你看。
基本概念:記憶體按字組擴展
EVM 的記憶體以 32 位元組為一個「字組」來計算。當你訪問某個記憶體位置 offset 時,EVM 會自動擴展記憶體到 offset + 32 位元組。
記憶體成本公式:
C_memory(n) = G_memory + Σ(i=0 to n-1) C_memory_word(i)
其中:
- n = 記憶體字組數量(ceiling(offset / 32) + 1)
- G_memory = 3 gas(基本調度成本)
- C_memory_word(i) = max(0, ceil((i+1)^2 / 512) - ceil(i^2 / 512))
化簡後的公式:
C_memory(n) = 3n + floor(n^2 / 512)
這個公式的神奇之處在於它的二次特性。讓我展開給你看:
假設 n = 32(即訪問第 32 個字組):
C_memory(32) = 3 * 32 + 32^2 / 512
= 96 + 1024 / 512
= 96 + 2
= 98 gas
假設 n = 64:
C_memory(64) = 3 * 64 + 64^2 / 512
= 192 + 4096 / 512
= 192 + 8
= 200 gas
看到了嗎?記憶體越大,每增加一個字組的邊際成本越高。這是故意設計的——防止有人申請超大的記憶體陣列來拖慢整個網路。
邊際成本推導:
邊際成本 = C_memory(n+1) - C_memory(n)
= [3(n+1) + (n+1)^2 / 512] - [3n + n^2 / 512]
= 3 + [(n^2 + 2n + 1) - n^2] / 512
= 3 + (2n + 1) / 512
所以當 n = 0 時,邊際成本是 3 + 1/512 ≈ 3.002 gas。當 n = 64 時,邊際成本是 3 + 129/512 ≈ 3.252 gas。
這解釋了為什麼你在不同位置讀取記憶體,gas 消耗可能不同。訪問越靠後的記憶體,成本越高。
2.5 存儲操作的 Gas 陷阱
SSTORE 是 gas 消耗的大坑,你必須搞懂它。
SSTORE 成本規則:
C_SSTORE(0 → non-zero) = 20000 gas // 首次寫入
C_SSTORE(non-zero → non-zero) = 5000 gas // 更新現有值
C_SSTORE(non-zero → 0) = 2900 gas // 刪除
Refund(non-zero → 0) = 15000 gas // 退還(上限為總消耗的 50%)
這個設計背後有經濟學原理:
- 首次寫入 20000 gas:需要創建新的狀態Trie節點,這是持久性存儲
- 更新 5000 gas:只需要修改現有節點,成本低很多
- 刪除 2900 gas:EVM 說「好吧你把空間騰出來了」,但創建免費刪除不給錢
等等,刪除收 2900,退還 15000?這豈不是白賺錢?
別高興太早,有兩個限制:
- 退還上限:退還的 gas 不能超過當前執行消耗的 50%。也就是說你至少要消耗兩倍於退還金額的 gas。
- 補貼取消:EIP-3529 把退還上限從 50% 調到了 20%,並移除了「SELFDESTRUCT 補貼」。
為什麼要有退還機制?
本來的想法是:如果你刪除一個 slot,那個空間就釋放了,未來其他合約可以覆蓋使用這塊空間。網路為創建付出了成本,你刪除應該得到補貼。
問題是有人濫用了這個機制。他們先創建大量空 slot,然後一口氣刪除,用退還的 gas 補貼正常交易的費用。這就是「gas 退還攻擊」。
實際例子:
// 這個合約的 gas 消耗很有趣
contract StorageTrick {
mapping(uint256 => uint256) public values;
// 寫入:20000 gas
function writeFirst(uint256 key, uint256 value) external {
values[key] = value;
}
// 更新:5000 gas
function update(uint256 key, uint256 value) external {
values[key] = value;
}
// 刪除:2900 gas,收 15000 退還
function erase(uint256 key) external {
delete values[key];
}
}
2.6 呼叫操作的 Gas 複雜度
CALL 系列是另一個需要小心的地方。
基本呼叫成本:
C_CALL = 700 gas(基礎成本)
加上:
- 轉移 ETH > 0:+9000 gas
- 目標為新帳戶(nonce = 0):+25000 gas
- cold access:+2600 gas(額外的訪問成本)
- warm access:+100 gas
等等,cold access 和 warm access 是什麼?
這是 EIP-2929 引入的概念。EVM 會追蹤哪些帳戶被訪問過:
- Cold Access:首次訪問某個帳戶,需要支付額外 2100 gas(其中 100 進入口令,2000 進 SLOAD)
- Warm Access:短時間內再次訪問,只需要 100 gas
數學推導:
C_CALL(cold, value=0, new=false) = 700 + 2100 + 0 + 0 = 2800 gas
C_CALL(cold, value>0, new=false) = 700 + 2100 + 9000 + 0 = 11800 gas
C_CALL(cold, value=0, new=true) = 700 + 2100 + 0 + 25000 = 27800 gas
C_CALL(warm, value=0, new=false) = 700 + 100 + 0 + 0 = 800 gas
所以如果你在一筆交易中多次調用同一個合約,從第二次開始就便宜很多。這就是為什麼有些合約設計「批處理」介面——減少重複的 cold access。
STATICALL 的特殊之處:
STATICALL 用於只讀調用,不能修改狀態。成本相對簡單:
C_STATICALL = 700 gas(如果目標已 warm)
C_STATICALL = 700 + 2100 = 2800 gas(如果目標 cold)
但好處是你不會觸發轉帳,所以不需要那 9000 gas。
2.7 日誌操作的 Gas 計算
LOG 系列用於發送事件,是鏈上日誌系統的基礎。
Gas 公式:
C_LOG(n) = 375 + 8 * num_topics + 8 * data_size / 32
其中:
- num_topics = 0, 1, 2, 3, 或 4
- data_size = 資料的位元組數
各類型 Gas:
| Opcode | 名稱 | Gas | 說明 |
|---|---|---|---|
| 0xa0 | LOG0 | 375 + 8 * words | 無 topic |
| 0xa1 | LOG1 | 375 + 8 + 8 * words | 1 個 topic |
| 0xa2 | LOG2 | 375 + 16 + 8 * words | 2 個 topic |
| 0xa3 | LOG3 | 375 + 24 + 8 * words | 3 個 topic |
| 0xa4 | LOG4 | 375 + 32 + 8 * words | 4 個 topic |
注意這裡的 words = ceil(data_bytes / 32)。
實際例子:
// ERC-20 Transfer 事件
event Transfer(address indexed from, address indexed to, uint256 value);
// 編譯後相當於:
// LOG3( // 3 個 topic
// topics[0] = keccak256("Transfer(address,address,uint256)")
// topics[1] = from
// topics[2] = to
// data = abi.encode(value)
// )
// Gas 消耗:
// 375 + 24 + 8 * 1 = 407 gas(假設 value < 2^256,只需要 1 個 word)
第三章:實戰最佳化技巧
3.1 記憶體 vs 存儲:何時用哪個?
這是個經常被問到的問題。讓我給你一個決策框架:
存儲(Storage)適合:
- 長期保存的資料
- 需要跨函數呼叫持久化的狀態
- 只有少數幾筆的資料
記憶體(Memory)適合:
- 函數內部暫存計算
- 大量資料的臨時處理
- 不需要持久化的資料
Calldata 適合:
- 函數參數(自動的)
- 不可修改的大型資料
contract MemoryVsStorage {
// 存儲:需要 20000 gas 首次寫入
uint256 public storedValue;
// 記憶體:只消耗記憶體成本
function computeWithMemory(uint256 x) public pure returns (uint256) {
uint256 y = x * 2; // 記憶體操作,約 3-6 gas
return y + 1;
}
// 糟糕模式:每次都寫入 storage
function badIncrement() external {
storedValue++; // 5000 gas(因為已存在)
}
// 好模式:記憶體計算,最後一次性寫入
function betterIncrement() external {
uint256 temp = storedValue; // 一次 SLOAD
temp++; // 記憶體加法
storedValue = temp; // 一次 SSTORE
}
// 更好的模式:減少 SSTORE 次數
function batchIncrement(uint256 times) external {
uint256 temp = storedValue;
temp += times; // 記憶體加法很便宜
storedValue = temp; // 只一次 SSTORE
}
}
3.2 使用 Assembly 優化循環
循環是 gas 消耗的大戶。讓我展示幾個優化技巧:
範例:不使用 assembly 的求和
function sumArrayBad(uint256[] storage arr) internal view returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}
使用 assembly 優化:
function sumArrayOptimized(uint256[] storage arr) internal view returns (uint256 total) {
assembly {
// 獲取陣列長度(從 slot 讀取)
let length := sload(arr.slot)
// 獲取陣列資料指標
let dataPtr := add(arr.offset, 0x20)
// 迴圈求和
for { let i := 0 } lt(i, length) { i := add(i, 1) } {
total := add(total, mload(add(dataPtr, mul(i, 0x20))))
}
}
}
為什麼 assembly 更快?
- Solidity 的
arr[i]每次都會重新計算存儲位置 - Assembly 直接用指標遞增,減少計算
- 避免 Solidity 的邊界檢查
3.3 位元運算替代乘除
這個技巧很多人不知道,但超級實用:
contract BitwiseOptimization {
// 乘以 2:左移 1 位比乘法便宜
function multiplyBy2(uint256 x) public pure returns (uint256) {
return x * 2; // MUL: 5 gas
}
function multiplyBy2Optimized(uint256 x) public pure returns (uint256) {
return x << 1; // SHL: 3 gas
}
// 除以 2:右移 1 位比除法便宜
function divideBy2(uint256 x) public pure returns (uint256) {
return x / 2; // DIV: 5 gas
}
function divideBy2Optimized(uint256 x) public pure returns (uint256) {
return x >> 1; // SHR: 3 gas
}
// 除以 4
function divideBy4(uint256 x) public pure returns (uint256) {
return x / 4; // DIV: 5 gas
}
function divideBy4Optimized(uint256 x) public pure returns (uint256) {
return x >> 2; // SHR: 3 gas
}
// 判斷奇偶
function isOdd(uint256 x) public pure returns (bool) {
return x % 2 == 1; // MOD: 5 gas
}
function isOddOptimized(uint256 x) public pure returns (bool) {
return x & 1 == 1; // AND: 3 gas
}
}
但要注意,這些優化只對 2 的冪次有效。x * 3 沒辦法用位移替代。
3.4 減少外部呼叫次數
外部呼叫(CALL、DELEGATECALL)超級貴。讓我展示幾個模式:
批量處理節省呼叫成本:
contract BatchCalls {
// 糟糕:多次外部呼叫
function badBatchCall(address[] calldata targets, uint256[] calldata values)
external
payable
{
for (uint256 i = 0; i < targets.length; i++) {
(bool success, ) = targets[i].call{value: values[i]}("");
require(success);
}
}
// 好一點:合併 Transfer
function betterBatchCall(address[] calldata targets, uint256[] calldata values)
external
payable
{
uint256 total = 0;
for (uint256 i = 0; i < targets.length; i++) {
total += values[i];
}
require(msg.value >= total);
for (uint256 i = 0; i < targets.length; i++) {
(bool success, ) = targets[i].call{value: values[i]}("");
require(success);
}
}
}
使用 Revert Reason 節省 Gas:
contract RevertReasons {
// 不提供 revert reason
function badRequire(bool condition) external pure {
require(condition); // revert 時不帶 reason
}
// 提供簡潔的 revert reason
function betterRequire(bool condition) external pure {
require(condition, "BAD"); // 比長字串便宜
}
// 使用自定義 error(最省 gas)
error Unauthorized();
function bestRequire(bool condition) external pure {
if (!condition) revert Unauthorized();
}
}
3.5 Immutable 和 Constant 的威力
immutable 和 constant 變數在部署時就確定了值,不佔用 storage:
contract ConstantsDemo {
// Constant:在部署時就確定了,不佔用 storage
uint256 public constant PRECISION = 1e18;
bytes32 public constant NAME = "MyToken";
// Immutable:在構造函數時設定一次,之後不可改
uint256 public immutable totalSupply;
address public immutable owner;
constructor(uint256 _totalSupply) {
totalSupply = _totalSupply;
owner = msg.sender;
}
// 壞例子:每次都從 storage 讀取
function badGetPrecision() public view returns (uint256) {
return PRECISION; // Solidity 會優化為常量,但不明確
}
// 好例子:直接使用常量
function goodGetPrecision() public pure returns (uint256) {
return 1e18; // 編譯時就確定了,零 gas
}
}
第四章:EOF 時代的新 Opcode
4.1 EOF 帶來了什麼?
EVM Object Format(EOF)是 EVM 史上最大的架構變革。雖然還沒完全激活,但值得提前了解。
EOF 的核心改變:
- 代碼和數據分離:代碼不能包含
JUMPDEST動態計算 - 強制驗證:跳轉目標在部署時就驗證
- 新 Opcode:
CALLF、RETF、JUMF等
新 Opcode 一覽:
| Opcode | 名稱 | Gas | 說明 |
|---|---|---|---|
| 0x5c | CALLF | 5 + G_call | 函數呼叫 |
| 0x5d | RETF | 3 | 函數返回 |
| 0x5e | JUMF | 5 | 間接跳轉 |
| 0x5f | RJUMP | 2 | 相對跳轉(短) |
| 0x60 | RJUMPI | 4 | 相對條件跳轉(短) |
RJUMP 和 RJUMPI 是固定偏移跳轉,不像 JUMP 和 JUMPI 那樣需要動態計算目標。這讓 EVM 可以更容易地驗證程式碼,也讓 JIT 編譯器更容易優化。
EOF Gas 模型:
// EOF 模式下的函數呼叫
contract EOFDemo {
function main() public {
uint256 x = helper(10); // CALLF
// ...
}
function helper(uint256 input) internal returns (uint256) {
return input * 2; // RETF
}
}
// Gas 消耗分析:
// CALLF: 5 gas
// helper 內部執行: ~10 gas
// RETF: 3 gas
// 總共: 18 gas
// 對比傳統 JUMP 方式:
// JUMP: 8 gas
// 函數內執行: ~10 gas
// JUMPI: 10 gas
// 總共: ~28 gas
// 節省約 35%
4.2 EIP-7702:EOA 的華麗轉身
EIP-7702 是另一個遊戲改變者。它允許普通 EOA 帳戶「借用」智慧合約的代碼。
原理:
當交易包含特殊的 magic 值時,發送者的 EOA 會臨時獲得指定合約的代碼:
// EIP-7702 交易格式
// 在交易之外額外攜帶:
// - 代理合約地址
// - 簽名(證明 EOA 同意綁定)
// 綁定後的 EOA 行為:
// - 代碼 = 代理合約代碼
// - 存儲 = EOA 原存儲 + 代理合約存儲(使用不同偏移)
// - 可以執行代理合約的任何函數
Gas 消耗:
EIP-7702 綁定:3000 gas
每次後續執行:標準 gas(根據 opcode)
這意味著 EOA 變成智慧合約只需要一次性支付 3000 gas,然後就可以享受智慧合約的全部功能——社交恢復、多簽、權限控制——而不需要遷移到新的合約位址。
第五章:常見陷阱與解決方案
5.1 避免 Gas 過高的模式
陷阱一:迴圈內的 SSTORE
// 糟糕:迴圈中每次都 SSTORE
function badBatchUpdate(uint256[] calldata values) external {
for (uint256 i = 0; i < values.length; i++) {
userValues[msg.sender][i] = values[i]; // 每個都寫入 storage
}
}
// 好:先全部計算,最後一次 SSTORE
function betterBatchUpdate(uint256[] calldata values) external {
uint256 temp;
for (uint256 i = 0; i < values.length; i++) {
temp += values[i]; // 記憶體計算
}
storedTotal[msg.sender] = temp; // 最後一次寫入
}
陷阱二:重複的外部呼叫
// 糟糕:每個代幣都呼叫一次
function distributeBad(address[] calldata tokens, address recipient) external {
for (uint256 i = 0; i < tokens.length; i++) {
IERC20(tokens[i]).transfer(recipient, amount); // 昂貴的外部呼叫
}
}
// 好:使用 multicall 模式
function multicallExample() external returns (bytes[] memory) {
// 用戶先 approve 一次
// 然後合約批量轉移
}
5.2 Gas 優化的度量方法
使用 Hardhat 的 gas reporter:
npx hardhat test --gas
或者自己寫個簡單的 profiler:
contract GasProfiler {
uint256 public gasBefore;
function startProfile() external {
gasBefore = gasleft();
}
function endProfile() external view returns (uint256) {
return gasBefore - gasleft();
}
// 使用示例
function profileOperation() external {
startProfile();
// 執行要測量的操作
expensiveFunction();
uint256 used = endProfile();
emit GasUsed(used);
}
event GasUsed(uint256 gasUsed);
}
結語:Gas 優化是一門藝術
好了,聊了這麼多,我想你應該對 EVM opcode 和 gas 消耗有了全新的認識。
說真的,gas 優化這件事沒有捷徑。你需要理解底層,才能寫出高效的合約。但好訊息是,大多數優化原則都是通用的——減少 storage 操作、使用 memory 暫存、合併外部呼叫、避免不必要的計算。
我見過太多工程師寫完合約就扔給測試環境,發現 gas 太貴才回頭優化。這種方式效率很低。更好的做法是從一開始就把 gas 納入設計考量。
最後給你幾個建議:
- 先把功能跑通:先確保邏輯正確,再考慮優化
- profiler 給你方向:用工具找到真正的瓶頸在哪裡
- 不要過度優化:有時候可讀性比 gas 節省更重要
- 測試網先行:主網 gas 貴,測試網免費,多跑幾遍再上
好了,廢話說完了。去折騰你的合約吧,記住 gas 優化這件事急不得,慢慢來。
References:
- Ethereum Yellow Paper: https://ethereum.github.io/yellowpaper/paper.pdf
- EVM Opcodes: https://www.evm.codes/
- EIP-1559: https://eips.ethereum.org/EIPS/eip-1559
- EIP-2929: https://eips.ethereum.org/EIPS/eip-2929
- EIP-1884: https://eips.ethereum.org/EIPS/eip-1884
- EIP-7702: https://eips.ethereum.org/EIPS/eip-7702
相關文章
- EVM Opcode 執行成本與 Gas 消耗深度技術分析:以太坊黃皮書規範引用與實際執行案例 — 本文深入分析以太坊虛擬機器(EVM)各類 Opcode 的 Gas 消耗模型,基於以太坊黃皮書的正式規範,提供每個操作碼的數學計算公式、複雜度分析以及實際執行成本案例。研究涵蓋從最基礎的棧操作到複雜的密碼學計算,幫助開發者建立精確的 Gas 估算能力。
- 以太坊虛擬機Opcode執行成本數學推導與量化分析完整指南 — 本文從量化工程師的視角,深入推導 EVM Opcode 執行成本的數學基礎。我們從計算理論角度分析不同 Opcode 的資源消耗,建立完整的 Gas 成本模型,包括記憶體擴展成本的二次函數特性、儲存操作的層級定價、密碼學操作的複雜度分析、以及密碼學預編譯合約的成本函數。同時提供 Solidity、Python 和 TypeScript 的實作程式碼範例。
- EVM Opcode 層級 Gas 優化完全指南:從底層原理到實戰技巧 — 深入理解 EVM Opcode 層面的 Gas 消耗機制,並據此進行優化,不僅可以顯著降低用戶的交易成本,還能提升合約的整體效率。本文從 EVM Opcode 的基礎出發,系統性地分析各類 Opcode 的 Gas 消耗特性,並提供大量可直接應用於實際項目的優化技巧。
- Solidity 位元運算優化完整指南:Gas 節省與智能合約效能極致優化 — 本指南從 EVM 機器碼層級出發,系統性地分析各類位元運算 opcode 的 Gas 消耗模型,提供可直接應用於生產環境的優化策略與程式碼範例。涵蓋定點數學與定標因子運算、位元遮罩與旗標操作、雜湊與簽章驗證優化、壓縮資料結構與位元封裝等進階主題。
- 以太坊 EVM Gas 計算數學推導:從 opcode 到複雜合約的深度量化分析 — 本文深入探討以太坊 EVM Gas 計算的數學原理,提供完整的 opcode 級別成本推導、SSTORE 的冷熱存儲成本模型、CALL 指令的複雜成本計算、以及 EIP-1559 費用模型的數學推導。包含大量 Python 程式碼範例和實際部署成本分析,幫助開發者理解 Gas 消耗的底層邏輯。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!