purescript-native编译器原理(二):MagicDo优化深潜——Effect/Eff/ST单子如何被内联成高效原生C++代码 📅 发布时间:2026/8/24 17:28:50 👁 浏览次数: purescript-native编译器原理(二):MagicDo优化深潜——Effect/Eff/ST单子如何被内联成高效原生C代码【免费下载链接】purescript-nativeA native compiler backend for PureScript (via C or Golang)项目地址: https://gitcode.com/gh_mirrors/pu/purescript-nativepurescript-native 是一个 PureScript 原生编译器后端能把 PureScript 代码直接编译成 C或 Golang。本文深入讲解它的核心MagicDo 优化——如何把Effect、Eff、ST单子的样板代码内联成高效原生 C 代码把单子bind/return的全部开销在编译期彻底消除。 为什么单子需要专门的内联优化在 JavaScript 后端do块被脱糖成嵌套的调用多一次调用只是多一层函数帧JS 引擎尚可优化。但在原生 C 后端这些间接调用是真实的函数调用 字典查找开销被放大。关键的观察是单子化monomorphized之后Effect/Eff/ST的和return结构完全同构——它们本质上只是取出前一个动作的值传给后续代码。既然结构固定编译器就可以在编译期把整个单子结构摊平直接生成直线代码。这就是 MagicDo 的立身之本。 三个通道magicDoEffect、magicDoEff、magicDoST核心实现位于src/CodeGen/IL/Optimizer/MagicDo.hs。同一个magicDo核心函数分别针对三个单子实例化优化通道目标单子典型场景magicDoEffectEffect不携带状态的副作用I/O、日志、数组操作magicDoEffEff可携带运行时的状态化效果magicDoSTST局部状态通常配合runST使用值得强调的是优化识别的不是字面函数名而是单子化后的类型类字典参数。例如判断一次调用是否属于Effect单子靠的是检查其字典参数是否为Effect.bindDict、函数本体是否为多态的Control.Bind.bind见src/CodeGen/IL/Optimizer/Common.hs中的isDict辅助函数。这保证了只对真正的单子绑定动手绝不误伤普通函数调用。 四条核心变换规则1.pure/return→ 直接取值return x不再构造任何单子包装直接变成值x本身。2.bind→ 局部变量 顺序语句核心规则m \x - body被改写为声明局部变量x m()然后顺序执行 body。源码注释中的示例最直观优化前单子调用形态Prelude(m1)(function(x) { return ...; })优化后直线语句形态function __do { var x m1(); ... }嵌套的单子调用链被彻底展平为顺序语句函数调用帧、字典参数全部消失。3.discard→ 丢弃绑定的结果let _ m这类执行但丢弃结果的写法会被识别为discard直接替换成执行 m、忽略返回值的语句块。4.untilE/whileE→ 原生 while 循环Effect 库的untilE与whileE组合子被直接脱糖为原生While循环 IL完全绕开函数调用循环判断与循环体由 C 编译器直接处理。还有一个不起眼的配角applyReturns内联过程中do块内部的每一个return e都会被替换为调用 e()。因为被内联进来的单子动作从被引用的值变成了必须被执行的调用——这一步保证了副作用顺序与原始语义严格一致。 ST 单子的专属深潜引用变成局部变量除通用通道外ST单子还有一个专属的inlineST通道思路更激进定位runST块作为分析的边界清点资源枚举该块内所有由newSTRef创建的引用以及所有readSTRef/writeSTRef/modifySTRef的使用点安全性判断只有当每个引用都只在本runST作用域内使用、且没有任何引用逃逸到外部比如作为函数参数传走或作为结果返回时才进入激进模式激进内联{ value: v }对象包装被完全拆掉STRef退化为普通局部变量——newSTRef v→ 声明局部变量并初始化readSTRef r→ 直接读取变量writeSTRef r v→ 直接赋值若任一检查不通过则保守地保留对象包装形式仅做安全的等价替换。这意味着一段纯就地更新的ST代码最终编译产物里没有堆分配、没有引用计数就是最朴素的局部变量读写。 管线全景MagicDo 在哪个环节工作src/CodeGen/IL/Optimizer.hs中的optimize函数定义了完整的优化管线MagicDo 的位置很讲究内联阶段函数组合内联、unsafeCoerce/unsafePartial内联untilFixedPoint循环至不动点MagicDo 阶段按magicDoEffect→magicDoEff→magicDoST的顺序每个通道都跑untilFixedPoint——因为剥开一层绑定后往往会暴露出下一层可继续内联的绑定收尾阶段tco尾调用优化、inlineST引用内联、删除无用结果、公共子表达式内联再次不动点迭代最终整理合并相邻的变量声明。顺序即语义MagicDo 必须先行。只有单子结构先被摊平成直线语句后续的尾调用优化才能认出循环结构C 端才能真正受益。 对开发者的实际意义放心写do记法Effect/Eff/ST的单子风格写得越多收益越大——内联后的 C 代码与手写直代码几乎无差别ST 是零成本抽象只要STRef不逃出runST作用域它就只是局部变量连shared_ptr都省了完全透明这些优化在pscpp编译流程中自动执行入口见app/Main.hs的transpile函数逐模块输出.h/.cpp开发者零感知、零配置。 关键源码速查MagicDo 优化实现含三个通道与inlineSTsrc/CodeGen/IL/Optimizer/MagicDo.hs完整优化管线与不动点循环src/CodeGen/IL/Optimizer.hs字典识别工具isDictsrc/CodeGen/IL/Optimizer/Common.hs编译器入口与输出app/Main.hsC 运行时支撑文件runtime/purescript.cpp、runtime/dictionary.h至此Effect/Eff/ST 的单子语法糖已在编译期化为乌有剩下的就是直线、无分配的原生代码。下一篇我们继续拆解 TCO 尾调用优化与 Inliner 内联通道看它们如何与 MagicDo 接力共同榨干每一个 CPU 周期。【免费下载链接】purescript-nativeA native compiler backend for PureScript (via C or Golang)项目地址: https://gitcode.com/gh_mirrors/pu/purescript-native创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考