对EVM学习的一个笔记

1.重新学习了以太坊虚拟机相关知识,从汇编的层次对EVM的优化进行研究,总结如下:
solidity中运行合约的成本基本上是由sstore指令和sload指令来主导,因此对状态变量的访问是非常昂贵的。
A.固定长度数据类型

  • 对于固定长度数据类型,如整型、结构体、由固定长度数据类型组成的固定长度的数组,在不进行优化的前提下,每一个元素都占有一个slot,并且从slot0开始依次排列。
  • 声明状态变量但不进行初始化,由于汇编之后不会有sstore和sload指令,因此并不会消耗gas。相反,只要进行了初始化操作,那么必然会形成sstore和sload等指令,所以就必然会消耗gas。总之,在solidity中对状态变量进行初始化是很关键的操作。
  • 虽然固定长度数组、结构体和状态变量在存储器中的布局是一样的,但是产生的汇编代码不同,这是因为solidity为数组的访问产生了边界检查代码。
  • 数组的边界检查会干扰编译器优化,比起存储变量和结构体,定长数组的效率更低。
  • 对于打包行为,加上编译优化选项开关后,可以将总和小于32字节的多个变量使用一个sstore命令存储在一个slot中,所以节省了gas。但如果在不同的函数中对多个变量进行访问,则无法达到优化效果,这是因为编译器只能优化一个标签内的东西,但无法跨标签进行优化。
  • 调用函数会让你消耗更多的成本,不是因为函数调用昂贵(他们只是一个跳转指令),而是因为sstore指令的优化可能会失败。
  • 即使定长数组与等效的结构体和存储变量的存储布局是一样的,优化也失败了。每个数组的访问都有一个边界检查代码,它们在不同的标签下被组织起来。优化无法跨标签,所以优化失败。
  • 总结:如果Solidity编译器能弄清楚存储变量的大小,它就会将这些变量依次的放入存储器中。如果可能的话,编译器会将数据紧密的打包成32字节的块。

B.动态数据类型

  • 包括映射、不定长数组、不定长字节数组,映射是其中最简单的数据类型,是的,的确如此,因此要尽量使用映射,而不是数组。
  • 映射计算存储地址的公式:keccak256(bytes32(key) + bytes32(position))
  • 对于常量键值key,编译器可以提前进行存储位置的计算,所以生成的汇编代码很简单,使用已计算好的位置就可以;但是如果使用一个变量来持有key值,那么会在内存中进行操作,把key和位置加载到相邻的内存块中来进行“连接”,然后给keccak256进行计算,成本取决于被hash的数据有多少,每个SHA3要30gas,每个32字节的字需要6gas,因此对于一个uint256类型的key,gas成本是30+6*2=42
  • 每个存储槽slot只能存储32字节,如果映射的值超过32字节,如一个大小超过32字节的结构体,那么结构体会在key+slot映射到的位置开始存储。
  • 考虑到映射的设计方式,每项需要的最小存储空间是32字节,即使你实际只需要存储1个字节,因此这相当浪费空间。
  • 动态数组是映射的升级。数组首个元素的存储位置也要通过进行keccak256(slot)得出,然后再加上索引index,才能得到index元素对应的存储位置。本质上跟访问一个映射的元素没有区别。
  • 动态数组只是比映射多个一点特征,让它看上去就像数组一样。
  • 数组与映射比较,数组的一个优势就是打包,但是由于数组要进行边界检查,所以对数组元素的访问并不会将sstore指令优化掉。
  • bytes和string是为字节和字符进行优化的特殊数组类型。如果数组的长度小于31字节,只需要1个存储槽来存储整个数组。长一点的字节数组跟正常数组的表示方式差不多。

总结:查看Solidity编译器的内部工作,可以看见熟悉的数据结构例如映射和数组与传统编程语言完全不同。
1数组跟映射一样,非高效
2比映射的汇编代码更加复杂
3小类型(byte,uint8,string)时存储比映射高效
4汇编代码优化的不是很好。即使是打包,每个任务都会有一个sstore指令
 

猜你喜欢

转载自blog.csdn.net/kugool/article/details/123429693