Solidity编程语言中归并排序(Merge Sort)的实现方法及Gas高效优化方案咨询
在Solidity中实现Gas高效的归并排序
嘿,我太懂在Solidity里折腾归并排序的痛苦了——毕竟EVM的Gas模型对内存操作、循环开销特别敏感,而归并排序本身的分治逻辑放在链上,稍不注意就会Gas超标。下面我来拆解下核心问题,再给你一个经过Gas优化的实现方案。
核心痛点:为什么原生归并排序在Solidity里Gas很高?
EVM对这几类操作收费特别狠,刚好归并排序很容易踩坑:
- 递归调用的栈开销:每次递归都要保存上下文,不仅Gas高,数组稍大还可能触发栈溢出(Solidity默认栈深度限制是1024)
- 频繁内存分配:递归版每次拆分都会新建子数组,内存分配的Gas成本会快速累积
- 存储(Storage)直接操作:如果直接在Storage数组上排序,读写成本是Memory的几百倍
推荐实现:迭代版归并排序(Gas友好型)
迭代版完全避开了递归的栈开销,还能更好地控制内存分配。下面是优化后的代码:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MergeSort { // 对外暴露的排序函数,接收Memory数组,返回排序后的数组 function sort(uint256[] memory arr) public pure returns (uint256[] memory) { uint256 arrLen = arr.length; if (arrLen <= 1) return arr; // 预分配临时数组,避免多次内存分配 uint256[] memory temp = new uint256[](arrLen); // 迭代步长从1开始,每次翻倍(模拟归并排序的分治过程) for (uint256 step = 1; step < arrLen; step *= 2) { uint256 leftStart = 0; while (leftStart < arrLen) { uint256 mid = leftStart + step; uint256 rightEnd = mid + step; // 处理数组边界,避免越界 if (rightEnd > arrLen) rightEnd = arrLen; if (mid > arrLen) mid = arrLen; // 归并两个有序子数组 _merge(arr, temp, leftStart, mid, rightEnd); leftStart = rightEnd; } // 将临时数组内容拷贝回原数组,准备下一轮归并 _copyArray(temp, arr, arrLen); } return arr; } // 归并两个有序子数组:arr[left..mid-1] 和 arr[mid..right-1] function _merge( uint256[] memory arr, uint256[] memory temp, uint256 left, uint256 mid, uint256 right ) internal pure { uint256 i = left; uint256 j = mid; uint256 k = left; // 双指针合并两个有序数组 while (i < mid && j < right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } // 拷贝剩余未合并的元素 while (i < mid) temp[k++] = arr[i++]; while (j < right) temp[k++] = arr[j++]; } // 复用数组拷贝逻辑,减少重复代码 function _copyArray( uint256[] memory src, uint256[] memory dest, uint256 length ) internal pure { for (uint256 i = 0; i < length; i++) { dest[i] = src[i]; } } }
关键Gas优化点解析
- 迭代替代递归:彻底消除递归调用的栈开销,也不会触发栈溢出问题,适合处理更大的数组
- 预分配临时内存:只在排序开始时创建一次临时数组,避免每次归并都新建数组的内存分配成本
- 减少拷贝次数:每一轮归并完成后只做一次全量拷贝,而非多次小范围拷贝
- 缓存边界值:提前计算数组长度、子数组边界,避免循环内重复读取数组长度的微小开销
- 使用internal pure函数:
_merge和_copyArray标记为internal pure,避免外部调用开销,同时编译器能做更多优化
针对Storage数组的额外优化
如果要排序Storage中的数组,绝对不要直接在Storage上操作(读写成本极高),正确流程是:
- 把Storage数组拷贝到Memory中
- 在Memory里完成排序
- 把排序后的Memory数组写回Storage
示例代码片段:
uint256[] public storageArr; function sortStorageArray() public { uint256 arrLen = storageArr.length; if (arrLen <= 1) return; // 1. 拷贝Storage数组到Memory uint256[] memory arr = new uint256[](arrLen); for (uint256 i = 0; i < arrLen; i++) { arr[i] = storageArr[i]; } // 2. Memory中排序 arr = sort(arr); // 3. 写回Storage for (uint256 i = 0; i < arrLen; i++) { storageArr[i] = arr[i]; } }
额外小技巧
- 优先处理固定长度数组:编译器对固定长度数组的优化空间更大,Gas成本比动态数组更低
- 使用Solidity 0.8+版本:0.8.x编译器内置了免费的溢出检查,对内存操作的优化也更到位
- 避免排序过程中做额外计算:比如不要在
_merge循环里调用其他函数,尽量把逻辑内联(复用的小函数除外)
内容的提问来源于stack exchange,提问作者BeginnerGod
相关产品推荐
相关产品推荐

