如何使用 GHC 在近机器级别可靠地影响生成的代码?
How to reliably influence generated code at near machine level using GHC?
虽然这听起来像是理论问题,但假设我决定投资并构建一个用 Haskell 编写的关键任务应用程序。一年后我发现我绝对需要提高一些非常薄的瓶颈的性能,这将需要优化接近原始机器能力的内存访问。
一些假设:
- 它不是实时系统 - 偶尔的延迟峰值是可以容忍的(来自中断、线程调度不规则、偶尔的 GC 等)
- 这不是数字问题 - 数据布局和缓存友好的访问模式最重要(避免指针追逐、减少条件跳转等)
- 代码可能与特定的 GHC 版本相关联(但没有分叉)
- 性能目标需要就地修改预分配的堆外数组,同时考虑对齐(C 字符串、位压缩字段等)
- 数据静态限制在数组中,即使需要也很少分配
GHC 提供什么机制来执行这种优化?我所说的可靠是指如果源代码更改导致代码不再执行,则可以在源代码中更正它而无需在汇编中重写它。
- 是否已经可以使用特定于 GHC 的扩展和库?
- 自定义 FFI 是否有助于避免 C 调用约定开销?
- 专用编译器插件可以通过受限源 DSL 来实现吗?
- 来自 "high-level" 程序集(LLVM?)的源代码生成器可以作为解决方案吗?
听起来您正在寻找未装箱的数组。 "unboxed" 在 haskell-land 中表示 "has no runtime heap representation"。您通常可以通过查看 core 表示(这是一个非常 haskell-like 语言,这是编译的第一阶段)。所以例如你可能会在核心输出中看到 Int#
,这意味着一个没有堆表示的整数(它会在寄存器中)。
在优化 haskell 代码时,我们会定期查看核心代码,并期望能够通过更改源代码(例如添加严格性注释,或摆弄一个函数,使得它可以内联)。这并不总是很有趣,但会相当稳定,尤其是当您固定编译器版本时。
回到未装箱的数组:GHC 在 GHC.Prim 中公开了很多低级 primops,特别是听起来你想要可变的未装箱数组(MutableByteArray
). The primitive
包将这些 primops 公开在稍微更安全的后面,更友好 API 并且是你应该使用的(并且取决于是否编写你自己的库)。
还有许多其他实现未装箱数组的库,例如 vector
,它们是在 MutableByteArray
上构建的,但关键是对该结构的操作不会产生垃圾并且可能会向下编译到相当可预测的机器指令。
如果您正在进行数字工作并且想使用特定指令或直接在汇编中实现某些循环,您可能还想查看 this technique。
GHC还有一个非常强大的FFI,你可以研究一下如何用C和interop编写部分程序; haskell 为此目的在其他结构中支持固定数组。
如果您需要比那些给您更多的控制权,那么 haskell 可能是错误的语言。从你的描述中无法判断你的问题是否属于这种情况(你的要求似乎自相矛盾:你需要能够编写一个仔细的缓存调整算法,但任意 GC 暂停都可以吗?)。
最后一点:您不能依赖 GHC 的本机代码生成器来执行任何低级强度降低优化,例如GCC 执行(GHC 的 NCG 可能永远不会知道 bit-twiddling hacks、自动矢量化等)。相反,您可以尝试使用 LLVM 后端,但无法保证您是否在程序中看到加速。
虽然这听起来像是理论问题,但假设我决定投资并构建一个用 Haskell 编写的关键任务应用程序。一年后我发现我绝对需要提高一些非常薄的瓶颈的性能,这将需要优化接近原始机器能力的内存访问。
一些假设:
- 它不是实时系统 - 偶尔的延迟峰值是可以容忍的(来自中断、线程调度不规则、偶尔的 GC 等)
- 这不是数字问题 - 数据布局和缓存友好的访问模式最重要(避免指针追逐、减少条件跳转等)
- 代码可能与特定的 GHC 版本相关联(但没有分叉)
- 性能目标需要就地修改预分配的堆外数组,同时考虑对齐(C 字符串、位压缩字段等)
- 数据静态限制在数组中,即使需要也很少分配
GHC 提供什么机制来执行这种优化?我所说的可靠是指如果源代码更改导致代码不再执行,则可以在源代码中更正它而无需在汇编中重写它。
- 是否已经可以使用特定于 GHC 的扩展和库?
- 自定义 FFI 是否有助于避免 C 调用约定开销?
- 专用编译器插件可以通过受限源 DSL 来实现吗?
- 来自 "high-level" 程序集(LLVM?)的源代码生成器可以作为解决方案吗?
听起来您正在寻找未装箱的数组。 "unboxed" 在 haskell-land 中表示 "has no runtime heap representation"。您通常可以通过查看 core 表示(这是一个非常 haskell-like 语言,这是编译的第一阶段)。所以例如你可能会在核心输出中看到 Int#
,这意味着一个没有堆表示的整数(它会在寄存器中)。
在优化 haskell 代码时,我们会定期查看核心代码,并期望能够通过更改源代码(例如添加严格性注释,或摆弄一个函数,使得它可以内联)。这并不总是很有趣,但会相当稳定,尤其是当您固定编译器版本时。
回到未装箱的数组:GHC 在 GHC.Prim 中公开了很多低级 primops,特别是听起来你想要可变的未装箱数组(MutableByteArray
). The primitive
包将这些 primops 公开在稍微更安全的后面,更友好 API 并且是你应该使用的(并且取决于是否编写你自己的库)。
还有许多其他实现未装箱数组的库,例如 vector
,它们是在 MutableByteArray
上构建的,但关键是对该结构的操作不会产生垃圾并且可能会向下编译到相当可预测的机器指令。
如果您正在进行数字工作并且想使用特定指令或直接在汇编中实现某些循环,您可能还想查看 this technique。
GHC还有一个非常强大的FFI,你可以研究一下如何用C和interop编写部分程序; haskell 为此目的在其他结构中支持固定数组。
如果您需要比那些给您更多的控制权,那么 haskell 可能是错误的语言。从你的描述中无法判断你的问题是否属于这种情况(你的要求似乎自相矛盾:你需要能够编写一个仔细的缓存调整算法,但任意 GC 暂停都可以吗?)。
最后一点:您不能依赖 GHC 的本机代码生成器来执行任何低级强度降低优化,例如GCC 执行(GHC 的 NCG 可能永远不会知道 bit-twiddling hacks、自动矢量化等)。相反,您可以尝试使用 LLVM 后端,但无法保证您是否在程序中看到加速。