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