优化技巧

  • beskay
  • 发布于 2023-08-25 18:50
  • 阅读 9

本文档详细介绍了Solidity智能合约开发中的各种Gas优化技巧,涵盖存储、错误处理、数学运算和函数调用等多个方面,包括避免初始化默认变量、存储打包、常量与不可变状态变量、缓存存储变量、使用unchecked{}、calldata代替memory等关键优化策略,旨在帮助开发者编写更高效、更节省Gas的Solidity合约。

优化

存储相关

由于存储操作是最昂贵的指令之一,因此也最有可能节省 gas。

错误处理

数学

函数

其他


不初始化默认变量

合约存储中所有位置的默认值为零(uint0boolfalse,地址为 0x00..00,等)。因此,使用默认值初始化变量并不必要,只会浪费 gas。

DefaultVars.sol

// 部署成本: 12666 gas
contract DefaultVarsOptimized {
    uint256 internal a;
    bool internal b;
    address internal c;
    bytes32 internal e;
}

// 部署成本: 19308 gas
contract DefaultVars {
    uint256 internal a = 0;
    bool internal b = false;
    address internal c = address(0);
    bytes32 internal e = bytes32("");
}

部署初始化默认变量的示例合约成本额外约 6.6k gas,相比于优化版。如果我们查看编译器生成的 Yul(forge inspect DefaultVars ir-optimized),可以看到原因:

/// @src 23:193:349  "contract DefaultVars {..."
let _1 := memoryguard(0x80)
mstore(64, _1)
if callvalue() { revert(0, 0) }
sstore(0x00, 0x00)
sstore(0x01, and(sload(0x01), not(sub(shl(168, 1), 1))))
sstore(0x02, 0x00)
let _2 := datasize("DefaultVars_29531_deployed")
codecopy(_1, dataoffset("DefaultVars_29531_deployed"), _2)
return(_1, _2)

有三个额外的 SSTORE 指令,每个成本 2.2k gas(另见 StorageTest 中的 testZeroToZero())。

为什么只有三个 sstore 指令,而我们总共有 4 个变量?答案是变量打包。编译器将 bool 和地址放在同一个存储槽中,因为它们都适合 32 字节。因此只需要一个 SSTORE 操作 -> 存储打包

forge 命令

  • forge test --mc DefaultVarsTest -vvvv - 运行 gas 测试
  • forge inspect DefaultVars ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

存储打包

我们可以通过将使用少于 32 字节的变量相邻放置来节省存储。

存储打包在读取或写入同一存储槽中的多个值时特别有用。在这种情况下,只需要一个 SLOAD 或 SSTORE 操作,从而将访问存储变量的成本降低一半或更多。这种情况通常出现在结构体中:

struct Entry {
    uint128 id;
    uint128 value;
}
Entry f;

// 执行成本: 22323 gas
function writeStruct() external {
    // 我们在存储两个变量,但只为一个 SSTORE 付费
    f = Entry(1, 2);
}

下面是 StoragePacking.sol 中函数 writeStruct 的 Yul 表示。编译器将这两个 uint128 变量放在同一个存储槽中,因此只需要一个 SSTORE 操作。id(0x01)存储在存储槽的低 128 位(右对齐),value(0x02)存储在存储槽的高 128 位(左对齐)。

case 0x33fe0dda { // writeStruct()
    // --snip--
    // store Entry(1, 2);
    sstore(4, 0x0200000000000000000000000000000001)
    return(mload(64), _2)
}

警告:减少大小类型

虽然存储打包通常节省 gas,但需要注意的是,它也可能增加 gas 使用。因为 EVM 每次以 32 字节为单位操作。如果元素小于 32 字节,EVM 必须使用更多操作将元素从 32 字节缩减到所需大小。例如,请参见 StoragePacking.sol 中的 writeUint128writeUint256 函数。

// 执行成本: 22306 gas
function writeUint256() external {
    ++b[0];
}

// 执行成本: 22557 gas
function writeUint128() external {
    // 写入单个减少大小的变量 比 写入 uint256 更昂贵,
    // 因为 EVM 总是以 32 字节为单位操作。
    ++c[0];
}

执行 writeUint128 的成本为 22,557 gas,而执行 writeUint256 的成本为 22,306 gas。通过检查 forge inspect StoragePacking ir-optimized 的输出,我们可以看到编译器执行了额外的位操作以将 uint256 变量的大小减少到 128 位。

case 0x102f49a5 { // writeUint256()
    // --snip--
    if eq(_3, not(0)) // 检查 b 是否大于 max(uint256)
    {
        mstore(_2, shl(224, 0x4e487b71))
        mstore(4, 0x11)
        revert(_2, 0x24)
    }
    sstore(/**  "b" */ 0x01, add(_3, /** "b" */ 0x01))
    /// @src 38:492:1251  "contract StoragePacking is StorageLayout {..."
    return(_1, _2)
}
case 0x11c3a83a { // writeUint128()
    // --snip--
    let _5 := 0xffffffffffffffffffffffffffffffff
    let value := and(_4, _5)
    if eq(value, _5) // 检查 c 是否大于 max(uint128)
    {
        mstore(_2, shl(224, 0x4e487b71))
        mstore(4, 0x11)
        revert(_2, 0x24)
    }
    // 我们有额外的按位操作将 c 存储在存储槽的低 128 位
    sstore(/** "c" */ 0x03, or(and(_4, not(0xffffffffffffffffffffffffffffffff)), and(add(value, 1), _5)))
    return(mload(64), _2)
}

forge 命令

  • forge test --mc StoragePackingTest -vvvv - 运行 gas 测试
  • forge inspect StoragePackingTest ir-optimized - 显示优化后的 Yul 汇编

参考

⬆ 返回顶部

使用常量和不可变状态变量

与常规状态变量相比,常量和不可变变量的 gas 成本要低得多。

对于常量变量,赋值给它的表达式会被复制到所有访问该变量的地方,并且每次都会重新评估。这允许进行局部优化。不可变变量仅在构造时评估一次,其值被复制到代码中访问它的所有位置。对于这些值,预留 32 字节,即使它们可以适应更少的字节。因此,常量值有时比不可变值便宜。

如果我们通过 forge debug Constant --sig "readConstant()"forge debug Immutable --sig "readImmutable()" 调试 Constant.sol 中的函数,可以看到,编译器将不可变变量替换为 PUSH32(value),将常量变量替换为 PUSH4(value)

// size(address) = 20 bytes, but 32 bytes are reserved for immutable variables
// 编译器将 `a` 替换为 `PUSH32(address)`
address immutable a;

// 4 bytes, will be replaced with `PUSH4(0xaabbccdd)`
bytes32 constant b = bytes32(hex"AABBCCDD");

// 152 gas
function readConstant() public pure returns (bytes32) {
    return c;
}

// 167 gas
function readImmutable() public view returns (address) {
    return a;
}

只有值类型(例如 boolintN/uintNaddressbytesNenum)可以声明为不可变。所有前面的类型也可以声明为常量,还包括 stringbytescontract 变量。

forge 命令

  • forge test --mc ConstantTest -vvvv - 运行 gas 测试
  • forge inspect ConstantTest ir-optimized - 显示优化后的 Yul 汇编
  • forge debug Constant --sig "readConstant()"
  • forge debug Immutable --sig "readImmutable()"

参考

⬆ 返回顶部

固定大小变量比动态大小变量更便宜

作为一般规则,使用 bytes 进行任意长度的原始字节数据,使用 string 进行任意长度的字符串(UTF-8)数据。如果可以将长度限制为特定字节数,请始终使用值类型(bytes1bytes32)中的一种,因为它们便宜得多。

数组也是如此:如果知道最多会有特定数量的元素,请始终使用固定数组而不是动态数组。原因是固定数组在存储中不需要长度参数,因此节省一个存储槽。

// 22260 gas
function setFixedArray() public {
    fixedArray[0] = 1;
}

// 44440 gas
function setDynamicArray() public {
    dynamicArray.push(1);
}

// 22244 gas
function setFixedBytes() public {
    fixedBytes = "test test test test test";
}

// 22748 gas
function setDynamicBytes() public {
    dynamicBytes = "test test test test test";
}

forge 命令

  • forge test --mc FixedSizeTest -vvvv - 运行 gas 测试
  • forge inspect FixedSizeTest ir-optimized - 显示优化后的 Yul 汇编

参考

⬆ 返回顶部

缓存存储变量

存储读取是昂贵的:第一次 SLOAD 成本 2.1k gas,所有其他 SLOAD 操作成本 100 gas。因此,如果在同一函数中多次读取一个变量,将存储变量缓存到内存中是个好主意。

在下面的示例中,我们计算存储数组中元素的总和。缓存数组长度和结果和可为我们每次函数调用节省大约 2k gas。

uint256[10] myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
uint256 sum;

// 45495 gas
function sumArrayOptimized() public  {
    uint256 length = myArray.length; // SLOAD
    uint256 localSum;

    for (uint256 i = 0; i < length; i++) {
        localSum += myArray[i]; // SLOAD
    }

    sum = localSum; // SSTORE
}

// 47506 gas
function sumArray() public {
    for (uint256 i = 0; i < myArray.length; i++) { // SLOAD
        sum += myArray[i]; // SSTORE + 2x SLOAD
    }
}

在优化的函数中,我们总共有 11 次 SLOAD 操作和 1 次 SSTORE 操作。在未优化的函数中,我们有 30 次 SLOAD 操作和 10 次 SSTORE 操作。然而,它们之间的 gas 成本差异相对较小。原因是优化器:它检测到在循环中保持不变的表达式,并将它们移到循环外。在我们的示例中,表达式 myArray.length 是不变的,因此被移到循环外。这将差距减少到额外 10 次 SLOAD 操作和 9 次 SSTORE 操作,每次成本 100 gas。

forge 命令

  • forge test --mc VariableCachingTest -vvvv - 运行 gas 测试
  • forge inspect VariableCachingTest ir-optimized - 显示优化后的 Yul 汇编

参考

⬆ 返回顶部

瞬态存储

瞬态存储是一个仅在调用执行期间可用的特殊存储区域。两个新操作码将添加到 EVM:

  • TLOAD (0x05c)
  • TSTORE (0x5d)

瞬态存储的 gas 成本远低于合约存储:两者均为 100 gas。瞬态存储的潜在用例是重入锁,从 5100 gas 降低到 300 gas。

abstract contract ReentrancyGuard {
    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"); // 之前: 2.1k gas (SLOAD); 之后: 100 gas (TLOAD)
        _status = _ENTERED; // 之前: 2.9k gas (SSTORE); 之后: 100 gas (TSTORE)
        _;
        _status = _NOT_ENTERED; // 之前: 100 gas (SSTORE); 之后: 100 gas (TSTORE)
    }
}

瞬态存储将在即将到来的 Cancun 更新中包含。

参考

⬆ 返回顶部

尽可能使用 unchecked{}(例如,在循环中)

Solidity 提供两种执行算术运算的方法:检查和不检查。检查操作在发生溢出或下溢时会抛出异常,而未检查操作则不会。

在 for 循环中使用 unchecked{} 特别有用,因为不可能在不先耗尽 gas 的情况下溢出(在正常情况下 -> 确保你的代码是安全的且不能溢出!)。

// 22352 gas
function increment() public {
    number++;
}

// 22247 gas
function incrementUnchecked() public {
    unchecked {
        number++;
    }
}

如果我们检查上述代码的 Yul 表示,我们可以观察到,在我们递增 number 时,会调用 increment_uint256(value) 函数。另一方面,unchecked { number++; } 直接编译为 sstore(_2, add(sload(_2), 1)),在不进行任何检查的情况下递增它。

let _2 := 0

case 0xc7fd0347 { // incrementUnchecked()
    // --snip--
    sstore(_2, add(sload(_2), 1))
    return(_1, _2)
}
case 0xd09de08a { // increment()
    // --snip--
    sstore(_2, increment_uint256(sload(_2)))
    return(mload(64), _2)
}

function increment_uint256(value) -> ret
{
    if eq(value, not(0)) // 溢出/下溢检查
    {
        mstore(0, shl(224, 0x4e487b71))
        mstore(4, 0x11)
        revert(0, 0x24)
    }
    ret := add(value, 1)
}

forge 命令

  • forge test --mc UncheckedTest -vvvv - 运行 gas 测试
  • forge inspect Unchecked ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

前增量与后增量

众所周知的 gas 优化技巧之一是使用 ++i 而不是 i++。前者略微便宜,因为 i++ 会在递增之前保存原始值,需要额外的 DUPPOP 操作,分别消耗 3 和 2 gas。

然而,这种优化适用于旧/遗留编译器。在新的基于 IR 的编译(via_ir = true)中,gas 成本的差异消失

// 22308 gas(使用旧代码生成)
// 22245 gas(--via-ir 启用)
function postIncrement() public {
    number++;
}

// 22303 gas(使用旧代码生成)
// 22245 gas(--via-ir 启用)
function preIncrement() public {
    ++number;
}

基于 IR 的编译(两个函数的代码相同):

case 0x016e4842 { // postIncrement()
    // --snip--
    sstore(_1, increment_uint256(sload(_1)))
    return(mload(64), _1)
}
case 0x5b59b0c8 { // preIncrement()
    // --snip--
    sstore(_1, increment_uint256(sload(_1)))
    return(mload(64), _1)
}

forge 命令

  • forge test --mc IncrementTest -vvvv - 运行 gas 测试
  • forge test --mc IncrementTest -vvvv --via-ir - 运行启用 IR 基于编译的 gas 测试
  • forge inspect Increment ir-optimized - 显示优化后的 Yul 汇编

参考

⬆ 返回顶部

外部函数使用 calldata 而不是内存

Calldata 比内存便宜。如果输入参数不需要被修改,请考虑在外部函数中使用 calldata:

// 391 gas
function argAsCalldata(string calldata name) external pure {}

// 515 gas
function argAsMemory(string memory name) external pure {}

如果使用 calldata,数据是通过 calldataload 直接读取的。另一方面,如果使用 memory,数据首先会被复制到内存中,使用额外的指令。

forge 命令

  • forge test --mc CalldataMemoryTest -vvvv - 运行 gas 测试

参考

⬆ 返回顶部

使用自定义错误

使用自定义错误提供了一种高效的方式来回退事务,无论是执行成本还是部署成本。

自定义错误使用错误语句定义,可以在合约内部和外部使用,包括接口和库。

以下示例演示了自定义错误的用法:

error OnlyOwner();

contract CustomError {
    address owner = msg.sender;

    function setOwner() public {
        if (msg.sender != owner) revert OnlyOwner();

        owner = msg.sender;
    }
}

检查 Yul,我们可以看到传统的 require 错误语句需要多个 mstore 操作,而自定义错误只需要一个(用于错误签名):

// require(msg.sender == owner, "Only owner can call this function");
if iszero(eq(caller(), and(_3, sub(shl(160, 1), 1))))
{
    mstore(_1, shl(229, 4594637))
    mstore(add(_1, 4), 32)
    mstore(add(_1, 36), 33)
    mstore(add(_1, 68), "Only owner can call this functio")
    mstore(add(_1, 100), "n")
    revert(_1, 132)
}

// error OnlyOwner()
if iszero(eq(caller(), and(_3, sub(shl(160, 1), 1))))
{
    mstore(_1, shl(224, 0x5fc483c5))
    revert(_1, 4)
}

我们还可以看到每个 32 字节的块(32 个字符)的回退字符串需要额外的 mstore 操作。这也引出了下一个 gas 优化技巧:短回退字符串。

forge 命令

  • forge test --mc CustomErrorTest -vvvv - 运行 gas 测试
  • forge inspect CustomError ir-optimized - 显示优化后的 Yul 汇编

参考

⬆ 返回顶部

短的回退字符串

尽量将错误字符串的长度保持在 32 个字符以下,如果你通过 require 语句处理错误,以限制 mstore 操作的使用。你的回退字符串越短,部署成本也越便宜。

// 部署成本: 53416 gas
contract RevertShort {
    address owner = msg.sender;

    // 2363 gas
    function setOwner() public {
        require(msg.sender == owner, "!owner");

        owner = msg.sender;
    }
}

// 部署成本: 60222 gas
contract RevertLong {
    address owner = msg.sender;

    // 2381 gas
    function setOwner() public {
        require(msg.sender == owner, "Only the contract owner can call this function!");

        owner = msg.sender;
    }
}

forge 命令

  • forge test --mc RevertStringsTest -vvvv - 运行 gas 测试
  • forge inspect RevertStrings ir-optimized - 显示优化后的 Yul 汇编

参考

⬆ 返回顶部

尽早回退

函数的执行成本是 gas。尽量尽早回退,以避免无用的执行成本。

// 225 gas
function earlyRevert() public {
    require(false, "Early revert");
}

// 111290 gas
function lateRevert() public {
    for (uint i = 0; i < 1000; i++) {
        // do nothing
    }
    require(false, "Late revert");
}

forge 命令

  • forge test --mc RevertEarlyTest -vvvv - 运行 gas 测试

⬆ 返回顶部

要求链

如果有多个需要检查的条件,建议不要使用 &&|| 将它们组合在一起。相反,使用要求链,其中每个条件都通过多个 require 语句单独检查。

然而,需要注意的是,如果在要求语句中使用错误字符串,则合约的部署成本将增加。

// 2279 gas
function requireChained() public payable {
    require(msg.sender == owner);
    require(msg.value == 0);
    require(block.timestamp < 1000_000);
}

// 2317 gas
function requireNotChained() public payable {
    require(msg.sender == owner && msg.value == 0 && block.timestamp < 1000_000);
}

forge 命令

  • forge test --mc RequireChainingTest -vvvv - 运行 gas 测试

⬆ 返回顶部

使用 < 或 > 而不是 <= 或 >=

在 Solidity 中,没有对像 >=&lt;= 这样的表达式的特定操作码。如果使用 >=&lt;=,编译器将生成一个额外的 iszero 操作码,消耗额外 3 gas。

因此,建议使用 &lt;> 操作符来检查一个值是否大于或小于另一个值,因为它们不需要额外的 iszero 操作码。

// 267 gas
function greater(uint256 a, uint256 b) external pure returns (bool) {
    return a > b;
}

// 270 gas
function greaterEqual(uint256 a, uint256 b) external pure returns (bool) {
    return a >= b;
}

我们可以在 Yul 中看到额外的 isZero 指令:

case 0x71343515 { // greater
    // --snip--
    mstore(memPos_1, /** @src 17:184:189  "a > b" */ gt(param_4, param_5))
    return(memPos_1, 32)
}
case 0x82a1f94b { // greaterEqual
    // --snip--
    mstore(memPos_2, /** @src 17:333:339  "a >= b" */ iszero(lt(param_6, param_7)))
    return(memPos_2, 32)
}

forge 命令

  • forge test --mc ComparisonTest -vvvv - 运行 gas 测试
  • forge inspect Comparison ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

在检查条件时短路

当使用 && 运算符检查多个条件时,将最有可能失败的条件放在前面。这样,如果第一个条件失败,第二个条件将不会被检查。

另一方面,当使用 || 运算符时,建议将最有可能成功的条件放在前面。这样,如果第一个条件成功(评估为 true),第二个条件将不会被评估,从而优化 gas 使用。

⬆ 返回顶部

使用位移乘以/除以 2 的幂

如果需要将一个数字除以或乘以 2 的幂,可以通过使用位移来优化操作。右移(>>)等同于除以 2,左移(&lt;&lt;)等同于乘以 2。

// 241 gas
function divide(uint256 a) external pure returns (uint256) {
    return a >> 2; // 除以 2^2 = 4
}

// 317 gas
function divide(uint256 a) external pure returns (uint256) {
    return a / 4;
}

注意:当启用基于 IR 的编译时(via_ir=true),此优化会被"优化掉"。在这种情况下,两个函数的编译输出相同且都费用 153 gas。

if eq(0x3e823f79, shr(224, calldataload(0)))
{
    if callvalue() { revert(0, 0) }
    if slt(add(calldatasize(), not(3)), 32) { revert(0, 0) }
    mstore(_1, shr(0x02, calldataload(4)))
    return(_1, 32)
}

forge 命令

  • forge test --mc BitShiftTest -vvvv - 运行 gas 测试
  • forge inspect BitShift ir-optimized - 显示优化后的 Yul 汇编
  • forge inspect NoBitShift ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

addmod() 和 mulmod()

在执行取模操作时,使用 addmod()mulmod(),它们将算术和取模操作结合为一个步骤。

// 274 gas
function addMod(uint256 a) external pure returns (uint256) {
    return addmod(a, 1, 2);
}

// 395 gas
function addMod(uint256 a) external pure returns (uint256) {
    return (a + 1) % 2;
}

// 296 gas
function mulMod (uint256 a) external pure returns (uint256) {
    return mulmod(a, 1, 2);
}

// 434 gas
function mulMod (uint256 a) external pure returns (uint256) {
    return (a * 1) % 2;
}

比较 Yul 中的两个 addMod 函数,可以明显看出,使用 addmodmulmod 更便宜:由于 addmodmulmod 设计为自动处理溢出,因此不需要额外的溢出检查。

case 0xb1d818a1 { // addModBad
    // --snip--
    let value := calldataload(4)
    if gt(value, add(value, 1))
    {
        mstore(_2, shl(224, 0x4e487b71))
        mstore(4, 0x11)
        revert(_2, 0x24)
    }
    mstore(_1, addmod(value, 1, 0x02))
    return(_1, 32)
}

case 0xb1d818a1 { // addModGood
    // --snip--
    mstore(_1, addmod(calldataload(4), 1, 0x02))
    return(_1, 32)
}

forge 命令

  • forge test --mc ModuloTest -vvvv - 运行 gas 测试
  • forge inspect ModuloGood ir-optimized - 显示优化后的 Yul 汇编
  • forge inspect ModuloBad ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

将函数声明为可支付

默认情况下,Solidity 中的函数是非支付的,这意味着它们不接受以太支付。然而,如果你明确地将函数声明为可支付,则编译器将省略调用该函数时的 msg.value == zero 检查。

// 部署成本: 9642 gas
contract Payable {
    constructor() payable {}

    // 74 gas
    function foo() external payable {}
}

// 部署成本: 12066 gas
contract NonPayable {
    constructor() {}

    // 98 gas
    function bar() external {}
}

上述函数的 Yul 表示:

case 0xc2985578 { // foo()
    if slt(add(calldatasize(), not(3)), _2) { revert(_2, _2) }
    return(_1, _2)
}
case 0xfebb0f7e { // bar()
    if callvalue() { revert(_2, _2) } // 此检查在 foo() 中缺失
    if slt(add(calldatasize(), not(3)), _2) { revert(_2, _2) }
    return(_1, _2)
}

需要注意的是,将函数声明为可支付可能存在安全风险。确保不要破坏合约的功能。

forge 命令

  • forge test --mc PayableTest -vvvv - 运行 gas 测试
  • forge inspect PayableCombined ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

函数顺序很重要

在调用函数时,EVM 跳过函数选择器列表,直到找到匹配项。函数选择器按十六进制顺序排列,每次跳跃消耗 22 gas。如果有很多函数,可以通过将最常调用的函数放在顶部来节省 gas。

例如,考虑以下合约:

contract FunctionOrder {
    function a() external pure{}

    function b() external pure{}

    function c() external pure{}

    function d() external pure{}
}

使用 forge inspect FunctionOrder methods 显示函数选择器:

{
  "a()": "0dbe671f",
  "b()": "4df7e3d0",
  "c()": "c3da42b8",
  "d()": "8a054ac2"
}

由于函数选择器按十六进制顺序排列,函数的顺序是 a, b, d, c。查看 Yul 代码,我们可以确认这一点:

{
    switch shr(224, calldataload(0))
    case 0x0dbe671f { external_fun_a() } // a
    case 0x4df7e3d0 { external_fun_a() } // b
    case 0x8a054ac2 { external_fun_a() } // d
    case 0xc3da42b8 { external_fun_a() } // c
}

forge 命令

  • forge test --mc FunctionOrderTest -vvvv - 运行 gas 测试
  • forge inspect FunctionOrder methods - 显示函数选择器
  • forge inspect FunctionOrder ir-optimized - 显示优化后的 Yul 汇编

⬆ 返回顶部

限制修饰符

当你在 Solidity 中添加一个函数修饰符时,修改后的函数代码会嵌入到修饰符中。如果同一个修饰符被多次使用,代码会被重复,增加字节码大小。

另一方面,内部函数会单独调用,节省部署中的字节码。内部函数会因函数调用而产生轻微的运行时开销。这意味着它们在执行成本上稍微贵一些,但在部署中节省了大量冗余字节码。

参考

⬆ 返回顶部

索引事件

你可以在 Solidity 中的事件中包含最多三个(匿名事件为四个)索引参数。这些索引参数存储在一个特殊数据结构中,称为“主题”,而不是事件日志的数据部分。事件的第一个主题总是事件签名,除非事件被声明为匿名,这样使用它们的成本最低。

使用索引参数可以有效地在过滤块序列时查找特定事件。每个包含在事件中的索引参数成本额外 375 gas。

事件的 gas 成本:
static_gas = 375
dynamic_gas = 375 * topic_count + 8 * size + memory_expansion_cost

根据参数类型的不同,将其声明为索引的成本也不同。例如,字符串参数因内存扩展成本高于 375 gas,因此将其声明为索引更便宜。

对于 uint256 参数则相反:将它们声明为非索引更高效。添加主题的成本比直接将 uint256 值存储在事件日志的数据部分更高。

// 1352 gas
function anonLog() public {
    emit AnonymousLog(1, 2, 3);
}

// 1817 gas
function logNum() public {
    emit LogNum(1, 2, 3);
}

// 2121 gas
function logNumIndexed() public {
    emit LogNumIndexed(1, 2, 3);
}

// 2286 gas
function logStringIndexed() public {
    emit LogStringIndexed("Hello", "World", "!");
}

// 3463 gas
function logString() public {
    emit LogString("Hello", "World", "!");
}

forge 命令

  • forge test --mc EventsTest -vvvv - 运行 gas 测试

参考

⬆ 返回顶部

  • 原文链接: github.com/beskay/gas-gu...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
beskay
beskay
江湖只有他的大名,没有他的介绍。