Solidity链上字体优化:避免bytes常量不必要的存储拷贝
链上字体Gas优化方案(汇编+Solidity)
核心思路
Solidity中bytes常量会被嵌入合约bytecode的只读数据段,无需拷贝到内存再存入storage,可通过汇编直接读取或写入该段数据,彻底消除冗余拷贝的Gas开销。以下是三种适配不同业务场景的方案:
方案1:完全跳过存储,直接返回代码段常量
若字体数据为固定值、无需动态修改,根本不需要写入storage,直接在视图函数中通过汇编读取代码段的常量返回,Gas消耗可从12M降至极低。
// 定义多份字体常量(编译时嵌入bytecode) bytes constant FONT_SERIF = hex"[你的字体十六进制数据]"; bytes constant FONT_SANS = hex"[你的字体十六进制数据]"; // 根据字体ID返回对应数据 function getFont(uint256 fontId) external view returns (bytes memory) { bytes memory result; assembly { let dataPtr := 0 let dataLen := 0 // 根据ID匹配对应常量 switch fontId case 0 { dataPtr := FONT_SERIF.slot // 常量在bytecode中的起始偏移 dataLen := FONT_SERIF.length // 常量长度 } case 1 { dataPtr := FONT_SANS.slot dataLen := FONT_SANS.length } // 可扩展更多case // 分配内存并复制数据 result := mload(0x40) mstore(0x40, add(result, add(dataLen, 0x20))) // 更新内存指针 mstore(result, dataLen) // 写入数据长度 codecopy(add(result, 0x20), dataPtr, dataLen) // 从代码段直接复制到内存 } return result; }
优势:无存储开销,读取效率拉满,扩展性可通过封装枚举/常量映射优化,无需手写二分查找。
方案2:仅存储注册状态,读取时直接取代码段数据
若需记录字体注册状态(如哪些字体已启用),但无需存储字体本身,可在storage中仅存字体ID与常量索引的映射:
// 存储已注册字体的常量索引 mapping(uint256 => uint256) public registeredFontIndices; // 字体常量数组 bytes constant[] internal FONT_LIBRARY = [ hex"[字体1数据]", hex"[字体2数据]" ]; // 注册字体(仅存索引,Gas消耗可忽略) function _registerFont(uint256 fontId, uint256 constantIndex) internal { require(constantIndex < FONT_LIBRARY.length, "Invalid constant index"); registeredFontIndices[fontId] = constantIndex; } // 根据ID返回字体数据 function pathOf(uint256 fontId) external view returns (bytes memory) { uint256 idx = registeredFontIndices[fontId]; require(idx != 0 || registeredFontIndices[fontId] == 0, "Font not registered"); bytes memory result; assembly { let target := FONT_LIBRARY[idx] let dataPtr := target.slot let dataLen := target.length result := mload(0x40) mstore(0x40, add(result, add(dataLen, 0x20))) mstore(result, dataLen) codecopy(add(result, 0x20), dataPtr, dataLen) } return result; }
优势:既保留注册逻辑,又避免大体积数据的存储开销,Gas消耗仅来自映射读写和代码段读取。
方案3:必须写入storage时,跳过内存直接拷贝
若业务逻辑要求字体数据必须存入storage,可通过汇编直接从代码段复制到storage,跳过内存中间层:
bytes constant TARGET_FONT = hex"[你的字体数据]"; mapping(uint256 => bytes) public fontStorage; // 注册字体(直接从代码段写入storage) function _register(uint256 fontId) internal { assembly { let dataPtr := TARGET_FONT.slot let dataLen := TARGET_FONT.length // 计算storage位置:fontStorage的slot + fontId的哈希 let baseSlot := keccak256(add(fontStorage.slot, 0x20), fontId) // 先写入数据长度 sstore(baseSlot, dataLen) // 分批复制数据到storage(每次32字节) let remaining := dataLen let offset := 0 let currentSlot := add(baseSlot, 1) for { } gt(remaining, 0) { } { let chunkSize := min(remaining, 0x20) // 从代码段读取32字节到临时内存 codecopy(0x0, add(dataPtr, offset), chunkSize) // 写入到storage sstore(currentSlot, mload(0x0)) // 更新指针 currentSlot := add(currentSlot, 1) offset := add(offset, chunkSize) remaining := sub(remaining, chunkSize) } } }
优势:比原方案(memory→storage)节省约50%以上的Gas,避免了大内存拷贝的开销。
注意事项
- Solidity版本兼容:
.slot和.length属性在Solidity 0.6+稳定支持,建议使用0.8.x版本。 - 合约大小限制:以太坊合约bytecode最大为24KB(EIP-170),若单字体数据过大,需拆分常量或使用代理合约存储不同字体。
- 只读限制:代码段为只读区域,常量必须是编译时确定的静态数据,无法动态修改。
内容的提问来源于stack exchange,提问作者Satoshi Nakajima
相关产品推荐
相关产品推荐

