【区块链 | EVM】深入理解学习EVM - 深入理解EVM操作码,让你写出更好的智能合约

news2025/1/13 2:30:11

那些非典型的开销导致经典的软件设计模式在合约编程语言中看起来既低效又奇怪。如果想要识别这些模式并理解他们导致效率变高/低的原因,你必须首先对以太坊虚拟机(即 EVM)有一个基本的了解。

你的一些编程“好习惯”反而会让你写出低效的智能合约。对于普通编程语言而言,计算机做运算和改变程序的状态顶多只是费点电或者费点时间,但对于 EVM 兼容类的编程语言(例如 Solidity 和 Vyper),执行这些操作都是费钱的!这些花费的形式是区块链的原生货币(如以太坊的 ETH,Avalanche 的 AVAX 等等...),想象成你是在用原生货币购买计算资源。

用于购买计算、状态转移还有存储空间的开销被称做 燃料(下文统称 gas )。 gas 的作用是确定交易的优先级, 同时形成一种能抵御【女巫攻击】(Sybil resistance)的机制 ,而且还能防止【停机问题】(halting problem)引起的攻击。

欢迎阅读我的文章 Solidity 基础 去了解 gas 的方方面面

这些非典型的开销导致经典的软件设计模式在合约编程语言中看起来既低效又奇怪。如果想要识别这些模式并理解他们导致效率变高/低的原因,你必须首先对以太坊虚拟机(即 EVM)有一个基本的了解。

什么是EVM?

如果你已经熟悉 EVM,请随时跳到下个部分: 什么是 EVM 操作码?

任何一个区块链都是一个基于交易的 状态机。 区块链递增地执行交易,交易完成后就变成新状态。因此,区块链上的每笔交易都是一次状态转换

简单的区块链,如比特币,本身只支持简单的交易传输。相比之下,可以运行智能合约的链,如以太坊,实现了两种类型的账户,即外部账户和智能合约账户,所以支持复杂的逻辑。

外部账户由用户通过私钥控制,不包含代码;而只能合约账户仅受其关联的代码控制。EVM 代码以字节码的形式存储在虚拟 ROM 中。

EVM 负责区块链上所有交易的执行和处理。它是一个栈机器,栈上的每个元素长度都是 256 位或 32 字节。EVM 嵌在每个以太坊节点中,负责执行合约的字节码。

EVM 把数据保存在 存储(Storage) 和 内存(Memory) 中。存储(Storage)*用于永久存储数据,而*内存(Memory)*仅在函数调用期间保存数据。还有一个地方保存了函数参数,叫做*调用数据(calldata),这种存储方式有点像内存,不同的是不可以修改这类数据。

在 Preethi Kasireddy 的文章中了解有关以太坊和 EVM 的更多信息 Ethereum 是如何工作的?。

智能合约是用高级语言编写的,例如 Solidity、Vyper 或 Yul,随后通过编译器编译成 EVM 字节码。但是,有时直接在代码中使用字节码会更高效(省gas)。

LooksRare 写的 TransferSelectorNFT 智能合约

EVM 字节码以十六进制编写。它是一种虚拟机能够解释的语言。这有点像 CPU 只能解释机器代码。

Solidity 字节码示例

什么是 EVM 操作码?

所有以太坊字节码都可以分解为一系列操作数和操作码。操作码是一些预定义的操作指令,EVM 识别后能够执行这个操作。例如,ADD 操作码在 EVM 字节码中表示为 0x01。它从栈中删除两个元素并把结果压入栈中。

从堆栈中移除和压入堆栈的元素数量取决于操作码。例如,PUSH 操作码有 32 个:PUSH1 到 PUSH32。 PUSH 在栈上 添加一个 字节元素,元素的大小可以从 0 到 32 字节。它不会从栈中删除元素。作为对比, 操作码 ADDMOD 表示 [模加法运算](Modular Addition and Subtraction - Modular Numbers and Cryptography - Library Guides at Centennial College of addition in modular,%2B d ( mod N ) .) ,它从栈中删除3个元素然后压入模加结果。请注意,PUSH 操作码是唯一带有操作数的操作码。

操作码示例

每个操作码都占一个字节,并且操作成本有大有小。操作码的操作成本是固定的或由公式算出来。例如,ADD 操作码固定需要3 gas。而将数据保存在存储中的操作码 SSTORE ,当把值从0设置为非0时消耗 20,000 gas,当把值改为0或保持为0不变时消耗 5000 gas。

SSTORE 的开销实际上会其他变化,具体取决于是否已访问过这个值。可以在这里找到有关 SSTORE 和 SLOAD 开销的完整详细信息:

为什么了解 EVM 操作码很重要?

想要降低 gas 开销,了解 EVM 操作码极其重要,这也会降低你的终端用户的成本。由于不同的 EVM 操作码的成本是不同的,因此虽然实现了相同结果,但不同的编码方式可能会导致更高的开销。了解哪些操作码是比较昂贵的,可以帮助你最大程度地减少甚至避免使用它们。你可以查看 以太坊文档 以获取 EVM 操作码及其相关 gas 开销的列表。

下面是一些考虑了 EVM 操作码开销的反直觉设计模式的具体示例:

用乘法而不是指数: MUL vs EXP

MUL 操作码花费 5 gas 用于执行乘法。例如,10 * 10 背后的算术将花费 5 gas。

EXP 操作码用于求幂,其 gas 消耗由公式决定:如果指数为零,则消耗10 gas。但是,如果指数大于零,则需要 10 gas 加上指数字节数的 50 倍。

一个字节是 8 位,一个字节可以表示 0 到 2⁸-1 之间的值(即0-255),两个字节可以表示 2⁸ 到 2¹⁶-1 之间的值,以此类推。因此,例如求 10¹⁸ 将花费 10 + 50 * 1 = 60 gas,而求 10³⁰⁰ 将花费 10 + 50 * 2 = 160 gas,因为来表示 18 需要一个字节,表示 300 需要两个字节。

从上面可以清楚地看出,在某些时候你应该使用乘法而不是求幂。下面一个具体的例子:

contract squareExample {
uint256 x;
constructor (uint256 _x) {
   x = _x;
 }
function inefficcientSquare() external {
   x = x**2;
 }
function efficcientSquare() external {
     x = x * x;
 }
}

inefficientSquare 和 eficcientSquare 两个方法都把状态变量 x 改为 x 的平方。然而,inefficientSquare 的算术开销为 10 + 1 * 50 = 60 gas,而 efficientSquare 的算术开销为 5 gas。

由于上述算术开销之外的原因,inefficientSquare 的 gas 费用平均比 efficientSquare 多 200 左右。

缓存数据:SLOAD & MLOAD

众所周知,缓存数据可以大规模地提升更好的性能。同样,在 EVM 上使用缓存也极端重要,即使只有少量操作,也会明显节省 gas。

SLOAD 和 MLOAD 两个操作码用于从存储和内存中加载数据。MLOAD 成本固定 3 gas,而 SLOAD 的成本由一个公式决定:SLOAD 在交易过程中第一次访问一个值需要花费 2100 gas,之后每次访问需要花费 100 gas。这意味着从内存加载数据比从存储加载数据便宜 97% 以上。

下面是一些节省潜在 gas 的示例代码:

contract storageExample {
uint256 sumOfArray;
function inefficcientSum(uint256 [] memory _array) public {
        for(uint256 i; i < _array.length; i++) {
            sumOfArray += _array[i];
        }
} 
function efficcientSum(uint256 [] memory _array) public {
   
   uint256 tempVar;
   for(uint256 i; i < _array.length; i++) {
            tempVar += _array[i];
        }
   sumOfArray = tempVar;
} 
}

合约 storageExample 有两个函数: inefficientSum 和 efficientSum

这两个函数都将 _array 作为参数,这是一个无符号整型数组。他们都会把合约的状态变量 sumOfArray 设置为 _array 中所有元素的总和。

inefficcientSum 使用状态变量进行计算。请牢记,状态变量(例如 sumOfArray)保存在 存储 中。

efficcientSum 在内存中创建一个临时变量 tempVar,用于计算 _array 中值的总和。然后将 tempVar 赋值给 sumOfArray

当传入的数组仅包含 10 个无符号整数时, efficientSum的 gas 效率比 inefficcientSum 高 50% 以上。

它们的效率随着计算次数的增加而增加:当传入 100 个无符号整数的数组时,eficcientSum 比 inefficcientSum 的 gas 效率高 300% 以上。

避免使用面向对象编程模型:CREATE 操作码

CREATE 操作码用于创建包含关联代码的新帐户(即智能合约)。它花费至少32,000 gas,是 EVM 上最昂贵的操作码。

最好尽可能减少使用的智能合约数量。这与典型的面向对象编程不同,在典型的面向对象编程中,为了可复用性和清晰性,鼓励定义多个类。

这是一个具体的例子:

下面是一段使用面向对象方法创建“vault”的代码。每个“vault”都包含一个 uint256 变量,并在构造函数中初始化:

contract Vault {
    uint256 private x; 
    constructor(uint256 _x) { x = _x;}
    function getValue() external view returns (uint256) {return x;}
}
//  Vault 结束
interface IVault {
    function getValue() external view returns (uint256);
} // IVault 结束

contract InefficcientVaults {
    address[] public factory;
    constructor() {}
    
    function createVault(uint256 _x) external {
        address _vaultAddress = address(new Vault(_x)); 
        factory.push(_vaultAddress);
    }
    
    function getVaultValue(uint256 vaultId) external view returns (uint256) {
        address _vaultAddress = factory[vaultId];
        IVault _vault = IVault(_vaultAddress);
        return _vault.getValue();
    }
} // InefficcientVaults 结束

每次调用 createVault() 时,都会创建一个新的 Vault 智能合约。存储在 Vault 中的值由传递给 createVault() 的参数决定。然后将新合约的地址存储在数组 factory 中。

这是另一段实现相同功能的代码,但用映射代替了创建:

contract EfficcientVaults {
  // 映射:vaultId => vaultValue
  mapping (uint256 => uint256) public vaultIdToVaultValue;
  
  // 下一个 vault 的 id
  uint256 nextVaultId;
  
  function createVault(uint256 _x) external {
      vaultIdToVaultValue[nextVaultId] = _x;
      nextVaultId++;
  }
  
  function getVaultValue(uint256 vaultId) external view returns (uint256) {
      return vaultIdToVaultValue[vaultId];
  }
} // EfficcientVaults 结束

每次调用 createVault() 时,参数都存储在一个映射中, 映射的 ID 由状态变量 nextVaultId 确定,而 nextVaultId 在每次调用 createVault() 时递增。

这种实现上的差异导致 gas 成本大幅降低。


EfficcientVaults 的 createVault() 与 IneficcientVaults 相比,效率提高了 61%,消耗的 gas 减少了约 76,300。

应该注意的是,在某些情况下在合约中创建新合约是可取的,并且通常是为了不可变性和效率。随着合约的大小增加,与合约的所有交互的交易成本也将增加。 因此,如果你希望在链上存储大量数据,最好通过多个单独的合约分离这些数据。除此之外,应避免创建新合约。

存储数据:SSTORE

SSTORE 是将数据保存到存储的 EVM 操作码。一般而言,当将存储值从零设置为非零时,SSTORE 花费 20,000 gas,当存储值设置为零时,SSTORE 花费 5000 gas。

由于这种成本的存在,在链上存储数据效率低下且成本高昂,应尽可能避免。

这种方法在 NFT 中最为常见。开发人员将 NFT 的元数据(图像、属性等)存储在去中心化存储网络(如 Arweave 或 IPFS)上,而不是将其存储在链上。唯一保存在链上的数据是一条指向元数据的链接。可通过所有 ERC721 合约内置的 tokenURI() 函数获得此链接。

tokenURI() 函数的标准实现。 (来源:OpenZeppelin)

例如无聊猿Bored Ape Yacht Club smart contract。 调用 tokenURI( ) 函数,传入 tokenId: 0, 函数返回以下链接: 

ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0

如果点击链接,你将看到 BAYC #0 元数据的 JSON 文件:

这些数据在OpenSea上很容易验证: OpenSea:

还应注意,由于存储成本,某些数据结构在 EVM 中根本不可行。例如,使用邻接矩阵表示图(a graph using an adjacency matrix) 是完全不可行的,因为它的空间复杂度是 O(V²) 。

以上所有代码都可以在我的*Github*上找到

感谢你阅读,希望你喜欢这篇文章!

如果有机会,我愿意介绍更多的 gas 优化和细微差别。要了解更多信息,我建议使用以下资源:

  • 变量压缩打包大法 和 内存数组优化 作者: Franz Volland
  • Solidity gas 优化技巧 和 Solidity 节省 gas 和字节码大小的魔法 作者: Mudit Gupta
  • EVM: 从 Solidity 到字节码, 内存和存储 作者: Ethereum 工程小组
  • 以太坊黄皮书

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/117896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux当中的Sersync实时同步服务及其实战举例

目录 一、实时同步 1.定义 2.原理 3.实时同步场景 4.实时同步工具 &#xff08;1&#xff09;sersync &#xff08;2&#xff09;Lysncd 二、实时同步实例 1.环境规划 2.配置思路 NFS存储服务如下&#xff1a; &#xff08;1&#xff09;安装NFS &#xff08;2&am…

40. 使用块的网络(VGG)

虽然AlexNet证明深层神经网络卓有成效&#xff0c;但它没有提供一个通用的模板来指导后续的研究人员设计新的网络。 在下面的几个章节中&#xff0c;我们将介绍一些常用于设计深层神经网络的启发式概念。 与芯片设计中工程师从放置晶体管到逻辑元件再到逻辑块的过程类似&#x…

Node.js--》三大常见模块的使用讲解

目录 fs文件系统模块 fs.readFile()方法 fs.writeFile()方法 readFile与writeFile的使用 fs模块路径动态拼接问题 path路径模块 path.join()方法 path.basename() path.extname() path.parse() http模块 req请求对象 res响应对象 解决中文乱码问题 响应不同内容…

Python实现猎人猎物优化算法(HPO)优化支持向量机回归模型(SVR算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 猎人猎物优化搜索算法(Hunter–prey optimizer, HPO)是由Naruei& Keynia于2022年提出的一种最新的优…

计算机系统基础实验 - 同符号浮点数加法运算/无符号定点数乘法运算的机器级表示

实验3 同符号浮点数加法运算/无符号定点数乘法运算的机器级表示 实验序号&#xff1a;3 实验名称&#xff1a;同符号浮点数加法运算/无符号定点数乘法运算的机器级表示 适用专业&#xff1a;软件工程 学 时 数&#xff1a;2学时 一、实验目的 1.掌握定点数乘法溢出的判定方法…

Kafka触发Rebalance的场景分析

文章目录前言触发Rebalance的原因1. 消费者成员发生变化2. 分区数发生变化3. 订阅Topic发生变化Rebalance全流程介绍场景一&#xff1a;新成员入组场景二&#xff1a;成员主动离组场景三&#xff1a;成员崩溃离组场景四&#xff1a;组成员提交位移前言 所谓Rebalance就是让Con…

Python小工具-复制嵌套目录下的多个word文档到指定目录

文章目录Python小工具-复制嵌套目录下的多个word文档到指定目录需求原始数据工具实现思路代码实现1-6 配置项目7 定义file_type_to_reduce_dir函数完成文件复制或移动8 定义list_dir_by_level函数完成遍历调用函数并执行待改进地方完整代码自我反省Python小工具-复制嵌套目录下…

全志 Linux 系统启动优化 启动优化速度方式 优化启动流程 优化uboot 优化kernel等

文章目录1 概述2 启动速度优化简介2.1 启动流程2.2 测量方法2.2.1 printk time2.2.2 initcall_debug2.2.3 bootgraph.2.2.4 bootchart2.2.5 gpio 示波器.2.2.6 grabserial.2.3 优化方法2.3.1 boot0启动优化2.3.1.1 非安全启动.2.3.1.2 安全启动2.3.2 uboot启动优化2.3.2.1 完全…

07、SpringCloud 系列:Alibaba - 介绍

SpringCloud 系列列表&#xff1a; 文章名文章地址01、Eureka - 集群、服务发现https://blog.csdn.net/qq_46023503/article/details/12831902302、Ribbon - 负载均衡https://blog.csdn.net/qq_46023503/article/details/12833228803、OpenFeign - 远程调用https://blog.csdn.…

一套ASP.NET优惠券领取微信小程序源码(前台+后台)

ASP.NET优惠券领取微信小程序源码&#xff08;前台后台&#xff09; 源码免费分享&#xff01;需要源码学习可私信我。 一、源码特点 1、这是一个微信小程序对接淘宝的淘宝客api自助搜索优惠券领取程序&#xff0c;简单易学。 2、后台采用asp.netMvc框架开发、实现了调用阿里妈…

Java I/O(五)NIO应用之Netty

Netty 目录Netty1 Netty概览2 Netty核心组件2.1 Bootstrap和ServerBootStrap&#xff08;启动引导类&#xff09;2.2 Channel&#xff08;网络操作抽象类&#xff09;2.3 EventLoop&#xff08;事件循环&#xff09;2.4 EventLoopGroup&#xff08;事件循环组&#xff09;2.7 C…

【Vue】创建 Vue 实例与对象配置、容器与实例的关系、插值延伸和 Vue 开发工具的初步使用

创建 Vue 实例 引入 Vue 注意在 Head 中 <script type"text/javascript" src"./vue.js"></script>另一个 javascript 中创建 Vue 实例&#xff0c;注意在 Body 尾部 <script type"text/javascript">const x new Vue() <…

12. 目前常用的四种信道复用方式:()、()、()和() ---- 计算机网络

目前常用的四种信道服用方式&#xff1a;&#xff08;频分复用&#xff09;、&#xff08;时分复用&#xff09;、&#xff08;码分复用&#xff09;和&#xff08;波分复用&#xff09; 知识点 复用&#xff08;multiplexing&#xff09;&#xff1a;就是在一个信道上传输多路…

java SE阶段面试题

目录 1、Java 的数据类型有哪些&#xff1f; 2、变量的三要素是什么&#xff1f;变量使用有什么要求&#xff1f; 3、基本数据类型变量和引用数据类型变量有什么区别&#xff1f; 4、Java 的运算符有几种意思&#xff1f; 5、Java 的自增、自减运算符在自增变量前后有什么区…

《计算机网络》——第三章知识点

第三章思维导图 链路层的信道类型 一对一:点对点信道 —对多:广播信道 链路层要解决的问题 封装成帧 透明传输 差错检测密封&#xff0c;透气性差 封装成帧就是在一段数据的前后部分添加首部和尾部&#xff0c;这样就构成了一个帧。接收端在收到物理层上交的比特流后&#xff…

Pandas.to_csv()函数及全部参数使用方法一文详解+实例代码

目录 前言 一、基础语法与功能 二、参数说明和代码演示 1.path_or_buf 选择文件/文件路径写入 2.sep 指定分隔符 3.na_rep 指定缺少数据表示 4.float_format 指定浮点型字符串输出格式 5. columns 指定要写入的列 6.header 是否需要写入列名 7.index 是否写入行名称&am…

【实时数仓】Sugar拉取数据展示、品牌销售排行接口、品类销售占比接口和热门商品SPU排名接口的实现

文章目录一 Sugar拉取数据展示1 内网穿透&#xff08;1&#xff09;作用&#xff08;2&#xff09;工具&#xff08;3&#xff09;本机ip地址&#xff08;4&#xff09;花生壳配置2 配置组件二 品牌销售排行接口1 Sugar配置&#xff08;1&#xff09;图表配置&#xff08;2&…

2022《粤语好声音-乐队风暴》全国总决赛圆满收官!

2022年12月17日&#xff0c;由广东珠江、盛娱星汇海选联合主办的2022《粤语好声音-乐队风暴》全国总决赛在广州增城1978电影小镇正式拉开帷幕。从海选到全国总决赛&#xff0c;2022《粤语好声音-乐队风暴》在21座城市中&#xff0c;通过线上线下双模式开展&#xff0c;历时6个月…

OpManager 虚拟化管理

什么是虚拟化 虚拟化是创建计算资源的虚拟形式&#xff0c;如计算机、服务器或其他硬件组件&#xff0c;或基于软件的资源&#xff08;如操作系统&#xff09;。虚拟化最常见的示例是在操作系统安装期间对硬盘进行分区&#xff0c;其中物理硬盘驱动器被拆分为多个逻辑磁盘以提…

重点 |中级软件设计师易混淆知识点 (1)

本文章总结了软件设计师考试易混淆知识点&#xff01;&#xff01;&#xff01; 帮助大家更好的复习&#xff0c;希望能对大家有所帮助 比较长&#xff0c;放了部分&#xff0c;需要可私信&#xff01;&#xff01; 易混淆点1&#xff1a;原、反、补码的运算 1、原码&#x…