用遗传算法自动搜索 Yul 优化器最佳优化步骤序列:Solidity yul-phaser 工具实战指南
【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity
yul-phaser是 Solidity 仓库内置的一个内部工具,用于自动寻找 Yul 优化器(Yul optimiser)的最优优化步骤序列。它把"为给定程序挑选优化步骤与排列顺序"这一被称为phase-ordering problem的难题建模为搜索问题,并使用遗传算法(genetic algorithm)在庞大的解空间中迭代求解。读完本文,你将掌握 yul-phaser 的工作原理、全部核心命令行参数与默认值、如何从零开始搜索、如何从上次的搜索结果续跑、如何分析某个特定序列的指标与优化效果,以及如何把搜到的序列通过--yul-optimizations直接交给solc使用。
yul-phaser 要解决的问题
Solidity 编译器在启用优化后会经过多个 Yul 优化阶段(optimisation steps),每个阶段各司其职:有的消除无用代码(如UnusedPruner)、有的做公共子表达式消除(如CommonSubexpressionEliminator)、有的展开函数(如FullInliner)等。
这些阶段的执行顺序对最终生成代码的质量影响显著,而可能的排列组合空间极其庞大——这就是经典的 phase-ordering problem。更棘手的是,从理论上讲,可能不存在一个对所有程序都最优的单一序列:某个序列对合约 A 效果极佳,对合约 B 却可能适得其反。因此,与其手工反复试验,不如让程序自动搜索。
yul-phaser 正是为此而生的内部工具(位于 tools/yulPhaser 目录,编译目标名为yul-phaser,由 tools/CMakeLists.txt 构建)。它不参与日常编译流程,而是作为开发者的"参数搜索器":输入一组 Yul 程序,输出一个(或一批)适合这些程序的优化步骤序列。
工作流程:从问题建模到遗传算法
三个核心抽象
从源码结构看,整个工具建立在三个核心抽象之上(均在 tools/yulPhaser 下):
- Program(程序):Program.h 负责读取并解析输入文件,将其编译为 Yul AST,并允许把一组合规的优化步骤应用到程序上。
- Chromosome(染色体):Chromosome.h 表示一条优化步骤序列,即种群中的一个个体。它内部把步骤序列编码成字符串(每个字符代表一个优化步骤,见后文),并且一旦创建便不可变——想"变异"只能基于旧染色体生成新染色体。
- FitnessMetric(适应度指标):FitnessMetrics.h 计算一个染色体(序列)作用于程序后的"得分":得分越低,序列越好。所有指标都是确定性的,只依赖染色体本身与指标状态。
遗传算法的运转方式
算法入口在 AlgorithmRunner:它持有当前种群,在每一轮中调用可插拔的GeneticAlgorithm::runNextRound()对种群做进化,并负责打印轮次反馈、去重(把重复染色体替换为随机新个体,可用--no-randomise-duplicates关闭)、以及按需把种群写入文件。
yulPhaser/GeneticAlgorithms.h 中实现了三种可选算法:
- GEWEP(
GenerationalElitistWithExclusivePools):每一轮将种群分为互斥的三部分——精英池直接保留,交叉池与变异池分别由精英个体通过交叉、变异生成新个体。这是默认算法。 - classic(
ClassicGeneticAlgorithm):经典三段式流程——按适应度比例选择(可重复选中同一染色体)、按概率配对做交叉、再按概率对每个基因逐一施加变异(基因随机化/删除/添加)。 - random(
RandomAlgorithm):每轮仅保留固定比例的精英,其余全部替换为随机生成的全新染色体。它不基于当前种群产生后代,相当于一个"带精英保留的随机搜索基线"。
三种算法的参数与默认值都在 Phaser.cpp 的命令行描述中定义(详见下文参数表)。
序列的简写表示:字母即优化步骤
优化步骤序列在 yul-phaser 中以字母字符串呈现,每个字符代表一个优化步骤。完整对照表见 docs/internals/optimizer.rst 的 "Optimizer Steps" 一节,例如:
| 缩写 | 完整步骤名 |
|---|---|
f | BlockFlattener |
c | CommonSubexpressionEliminator |
D | DeadCodeEliminator |
i | FullInliner |
u | UnusedPruner |
g | FunctionGrouper |
h | FunctionHoister |
a | SSATransform |
v | EquivalentFunctionCombiner |
t | StructuralSimplifier |
注意:
BlockFlattener、FunctionGrouper、ForLoopInitRewriter三个步骤是其他步骤的前提,Yul 优化器总会先于用户指定步骤应用它们(见 docs/internals/optimizer.rst 说明),因此 yul-phaser 在--prefix说明中也提到它会隐式添加hgo前缀,确保染色体可以包含任意优化步骤。
此外,[...]中的步骤会被循环重复应用,直到 Yul 代码不再变化或达到最大轮数(当前为 12)——这个机制直接复用到solc的--yul-optimizations中(详见 docs/internals/optimizer.rst)。
快速上手:一次最简单的搜索
yul-phaser 对大多数参数都有合理的默认值,最简单的调用只需给出输入文件与随机种群大小:
tools/yul-phaser ../test/libyul/yulOptimizerTests/fullSuite/*.yul \ --random-population 100前提是你已经编译了 Solidity 源码树(从而生成tools/yul-phaser可执行文件),并且当前位于该工作副本的构建目录中。输入是一个或多个 Yul 程序,每个候选序列都会被应用到所有这些程序上,再根据所选指标打分。
--random-population 100表示初始种群包含 100 条随机生成的序列。从 PopulationFactory 的实现可以看到,初始种群支持三种来源,且可以组合:--population(显式指定序列)、--random-population(随机生成)、--population-from-file(从文件读取),最终种群是三者的并集。
运行yul-phaser --help可以查看全部可用选项(帮助文本由 buildCommandLineDescription 生成)。
常用工作模式
从上次搜索结果继续
搜索可能需要很长时间,yul-phaser 支持每轮结束后把当前种群保存到文件,中断后可无缝续跑:
tools/yul-phaser *.yul \ --random-population 100 \ --population-autosave /tmp/population.txt停止应用后,用--population-from-file读回种群继续搜索,同时继续自动保存(保存文件会随每轮更新):
tools/yul-phaser *.yul \ --population-from-file /tmp/population.txt \ --population-autosave /tmp/population.txt保存文件是纯文本格式:每行一条染色体(即一串字母)。这来自 buildFromFile 的实现——它按行读取文件并把每行当作一个基因序列。
分析一条给定序列
如果已经有了一条序列(无论是手工构造的还是搜出来的),可以用--rounds 0跳过算法迭代,让 phaser 只做"分析"工作。查看某条序列在指定指标下的得分:
tools/yul-phaser *.yul \ --show-initial-population \ --rounds 0 \ --metric code-size \ --metric-aggregator sum \ --population <your sequence>--rounds 0:算法运行 0 轮,即不进化;--metric code-size:使用程序大小指标(默认是relative-code-size);--metric-aggregator sum:把该序列对每个输入程序的得分求和(默认为average);--population <your sequence>:把目标序列作为初始(也是唯一)种群。
查看该序列优化后的程序长什么样:
tools/yul-phaser *.yul \ --rounds 0 \ --mode print-optimised-programs \ --population <your sequence>--mode共有三种取值(见 Phaser.h):
run-algorithm:默认模式,运行遗传算法;print-optimised-programs:打印每个染色体优化后的程序代码(对应 printOptimisedProgramsOrASTs 中operator<<输出的 Yul 文本);print-optimised-asts:打印优化后的程序 AST(JSON 形式,对应同一函数中的toJson()分支)。
使用 solc 生成的中间表示
solc可以把 Solidity 合约编译成 Yul IR,而这些输出可以直接作为 yul-phaser 的输入:
solc/solc <sol file> --ir --output-dir <output directory>输出目录中会出现一个或多个.yul文件。注意这些文件包含的是完整的 Yul 对象(object),而不仅是裸的 Yul 程序——yul-phaser 已经准备好处理这种情况(Program::load 负责解析包含 object 的输入)。这一步是"针对你自己的合约搜索优化序列"的标准做法:把项目里有代表性的合约转成 IR,喂给 phaser。
把搜索结果应用到编译器
搜索结束后,把得到的序列直接通过--yul-optimizations交给solc,让 Yul 优化器按该序列执行优化:
solc/solc <sol file> --optimize --ir-optimized --yul-optimizations <sequence>该选项的语义与默认序列的差异在 docs/internals/optimizer.rst 中有详细说明:默认情况下优化器应用预定义的步骤序列;传入--yul-optimizations后即覆盖之。例如文档中的示例'dhfoD[xarrscLMcCTU]uljmul:fDnTOcmu'——其中方括号内的部分会被循环应用。
完整命令行参数参考
以下参数及默认值均直接取自 Phaser.cpp 中buildCommandLineDescription()的定义(yul-phaser --help的输出同源)。
通用参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--help | — | 显示帮助信息并退出 |
--input-files <PATH> | 必填 | 输入文件(位置参数,可多个) |
--prefix <CHROMOSOME> | 空字符串 | 自动应用于每个输入程序的初始优化步骤,结果视为实际输入;这些步骤不属于染色体、不会被变异;相对指标的基准也是应用这些步骤之后的程序。phaser 总是隐式加hgo前缀,本选项值在其后 |
--seed <NUM> | 随机生成 | 随机数生成器种子(配合--show-seed可复现实验) |
--rounds <NUM> | 无限制 | 算法停止前的轮数 |
--mode <NAME> | run-algorithm | 运行模式,见上文三种取值 |
算法通用参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--algorithm <NAME> | GEWEP | 可选GEWEP、classic、random |
--no-randomise-duplicates | 关闭 | 默认每轮后把重复染色体替换为随机个体;开启后禁用该后处理 |
--min-chromosome-length <NUM> | 100 | 随机染色体最小长度 |
--max-chromosome-length <NUM> | 100 | 随机染色体最大长度 |
--crossover <NAME> | uniform | 交叉算子:single-point、two-point、uniform |
--uniform-crossover-swap-chance <PROBABILITY> | 0.5 | uniform 交叉中两个基因互换的概率 |
GEWEP 算法参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--gewep-mutation-pool-size <FRACTION> | 0.25 | 每轮由变异再生的种群比例 |
--gewep-crossover-pool-size <FRACTION> | 0.25 | 每轮由交叉再生的种群比例 |
--gewep-randomisation-chance <PROBABILITY> | 0.9 | 选择基因随机化作为变异操作的概率 |
--gewep-deletion-vs-addition-chance <PROBABILITY> | 0.5 | 未选择随机化时,选择基因删除而非添加的概率 |
--gewep-genes-to-randomise <PROBABILITY> | 1/max-chromosome-length | 基因随机化中任意基因被变异的概率 |
--gewep-genes-to-add-or-delete <PROBABILITY> | 1/max-chromosome-length | 基因添加(或删除)中某个基因被添加/删除的概率 |
classic 算法参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--classic-elite-pool-size <FRACTION> | 0.25 | 精英占种群比例,每轮直接保留 |
--classic-crossover-chance <FRACTION> | 0.75 | 染色体被选中参与交叉的概率 |
--classic-mutation-chance <FRACTION> | 0.01 | 基因被随机化的概率 |
--classic-deletion-chance <PROBABILITY> | 0.01 | 基因被删除的概率 |
--classic-addition-chance <PROBABILITY> | 0.01 | 随机基因被添加的概率 |
random 算法参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--random-elite-pool-size <FRACTION> | 每轮保留 1 个个体(与种群大小无关) | 每轮保留的种群比例 |
种群参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--population <CHROMOSOMES> | 空 | 加入初始种群的显式序列;可空格分隔多个值或多次指定该选项 |
--random-population <SIZE> | 空 | 加入初始种群的随机染色体数量 |
--population-from-file <FILE> | 空 | 从文本文件(每行一条染色体)读取并加入初始种群 |
--population-autosave <FILE> | 禁用 | 每轮结束后把种群写入指定文件 |
指标参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--metric <NAME> | relative-code-size | 适应度指标:code-size(程序绝对大小)或relative-code-size(相对原程序的比值) |
--metric-aggregator <NAME> | average | 多个程序得分如何合并:average、sum、maximum、minimum |
--relative-metric-scale <EXPONENT> | 3 | 相对指标的定标因子指数:指标为整数,相对值乘以 10^exp 后取整,如 exp=3 时 0.5→500、1.0→1000。因子越大越能区分细微差异,但数值可读性变差且大数可能丢精度 |
--chromosome-repetitions <COUNT> | 1 | 染色体代表的步骤序列被重复应用的次数 |
指标权重参数
相对/绝对代码大小指标按 Yul AST 节点计"成本"(CodeWeights)。默认权重如下(见 Phaser.cpp 的 "METRIC WEIGHTS" 一节),源码注释指出这组权重是过渡方案,只为保证任何语句/表达式都不为零成本:
| 参数 | 默认值 |
|---|---|
--expression-statement-cost | 1 |
--assignment-cost | 1 |
--variable-declaration-cost | 1 |
--function-definition-cost | 1 |
--if-cost | 2 |
--switch-cost | 1 |
--case-cost | 2 |
--for-loop-cost | 3 |
--break-cost | 2 |
--continue-cost | 2 |
--leave-cost | 2 |
--block-cost | 1 |
--function-call-cost | 1 |
--identifier-cost | 1 |
--literal-cost | 1 |
缓存与输出参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--program-cache | 关闭 | 缓存染色体前缀对应的中间程序,大幅加速适应度评估,但染色体较长时极其消耗内存;默认关闭(目前无法设置内存上限),内存充足时强烈建议开启 |
--show-initial-population | 关闭 | 算法开始前打印初始种群 |
--show-only-top-chromosome | 关闭 | 每轮只打印最优染色体而非整个种群 |
--hide-round | 关闭 | 隐藏轮次信息(轮数与已用时间) |
--show-cache-stats | 关闭 | 每轮后打印缓存大小与命中情况 |
--show-seed | 关闭 | 打印选中的随机种子 |
其中--program-cache的实现位于 ProgramCache.h:由于优化步骤是顺序应用的,前缀相同的序列共享相同的中间程序,缓存这些中间结果可以避免大量重复优化计算,代价是内存占用随染色体长度显著上升。
如何选好参数:来自实验的实用建议
挑选遗传算法参数并不简单,但 phaser 的默认值通常已足够在给定程序集上找到"与经验丰富的开发者手工调优结果相当甚至更好"的序列。真正困难的其实是提供有代表性的输入文件集:
- 如果输入文件用不到某些优化,工具会倾向产出不使用这些优化的序列,遇到能受益于它们的程序时会表现很差;
- 反之,如果所有输入文件都极度依赖某个优化,找到的序列可能对不依赖它的程序不友好。
因此,输入文件集的代表性比参数微调更重要。在默认值基础上,以下经验结论来自维护者基于一组粗略实验的总结(相关讨论见 Solidity 问题 #7806),供参考:
- 表现最好的算法是GEWEP(因此它也是默认算法)。
- 初始种群使用更长的序列效果更好——算法本身擅长裁剪多余步骤。
- 保留上一轮的最优序列有助于提升结果;尤其在使用
classic算法时,精英池(elite)至少应包含少量个体。 - 不要把变异/删除/添加概率设得太高:过高会破坏交叉保留的良好模式。1%–5% 左右的取值似乎效果最好(classic 算法默认的 0.01 正落在这个区间)。
- 让算法至少运行 1000 轮以上。通常它能更快找到好序列,但运行更久可以显著缩短序列长度——当以长序列起步时这一点尤其重要。
源码阅读指引
如果你想深入理解实现细节,推荐按以下路径阅读:
- Phaser.cpp:命令行解析、各组件工厂(
GeneticAlgorithmFactory、FitnessMetricFactory、PopulationFactory、ProgramFactory、ProgramCacheFactory)与总调度逻辑; - GeneticAlgorithms.h:三种遗传算法的接口与选项结构;
- Chromosome.h:染色体(基因串)与优化步骤序列之间的编码/解码(
genesToSteps/stepsToGenes); - FitnessMetrics.h:
ProgramSize、RelativeProgramSize以及四种聚合器(average/sum/maximum/minimum); - Population.h:种群数据结构与
makeRandom; - Mutations.h:基因随机化、基因添加、基因删除三类变异算子;
- AlgorithmRunner.h:轮次循环、去重、自动保存与输出反馈。
对应的 Yul 优化器全貌(所有优化步骤的详细说明)见 docs/internals/optimizer.rst,步骤缩写表见其中的 "Optimizer Steps" 一节;Yul 语言本身的文档见 docs/yul.rst。
小结
yul-phaser 把"Yul 优化步骤排序"这一组合优化问题交给遗传算法求解,并提供了从搜索、续跑、分析到与solc联动的完整工作流。它的正确使用姿势可以概括为三步:准备有代表性的 Yul 程序集(最好来自你自己的合约、经solc --ir转换)、以默认参数跑足够多轮(必要时调大初始序列长度与精英池)、把搜到的序列通过--yul-optimizations应用到编译器,从而为特定合约量身定制优化管线。
【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考