如何使用GHC在机器附近可靠地影响生成的代码?

时间:2018-08-29 23:17:26

标签: haskell ghc micro-optimization

虽然这听起来可能是理论上的问题,但假设我决定投资并构建用Haskell编写的关键任务应用程序。一年后,我发现我绝对需要提高一些非常薄的瓶颈的性能,这将需要优化接近原始计算机功能的内存访问。

一些假设:

  • 这不是实时系统-偶尔的延迟尖峰是可以容忍的(由于中断,线程调度异常,偶然的GC等)
  • 这不是数字问题-数据布局和缓存友好的访问模式最为重要(避免追逐指针,减少条件跳转等)
  • 代码可能与特定的GHC版本有关(但没有分叉)
  • 性能目标要求在考虑到对齐方式(C字符串,位打包字段等)的情况下就地修改预分配的非堆阵列
  • 数据静态地限制在数组中,并且很少需要分配

GHC提供什么机制来执行这种优化?可靠地说,我的意思是,如果源更改导致代码不再执行,则它在源代码中是正确的,而无需在程序集中重写。

  • 使用GHC特定的扩展和库是否已经可能?
  • 自定义FFI是否有助于避免C调用约定的开销?
  • 专用编译器插件可以通过受限制的源DSL来实现吗?
  • “高级”程序集(LLVM)的源代码生成器是否可以解决?

1 个答案:

答案 0 :(得分:2)

听起来您正在寻找未装箱的阵列。 haskell-land中的“ unboxed”表示“没有运行时堆表示形式”。通常,您可以通过查看 core 表示形式(这是一种非常类似于haskell的语言,可以了解是否将代码的某些部分编译为未装箱的循环(不执行分配的循环))那是编译的第一阶段)。所以例如您可能会在核心输出中看到Int#,这意味着它没有堆表示形式(将在寄存器中)。

在优化haskell代码时,我们会定期查看核心,并期望能够通过更改源代码(例如添加严格性注释或摆弄可以内联的函数)来操纵或纠正性能下降。这并不总是很有趣,但是会相当稳定,尤其是当您固定编译器版本时。

返回未装箱的数组:GHC在GHC.Prim中公开了许多低级primop,尤其是听起来像您想要可变的未装箱的数组(MutableByteArray)。 primitive软件包将这些primops公开在稍微更安全,更友好的API之后,这是您应使用的(并取决于是否编写自己的库)。

还有许多其他实现未装箱数组的库,例如vector,它们建立在MutableByteArray上,但要点是,对该结构的操作不会产生垃圾,并且可能会编译成漂亮的可预测的机器指令。

如果您正在执行数字运算,并且想要使用特定指令或直接在汇编中实现某些循环,您可能还想签出this technique

GHC还具有非常强大的FFI,您可以研究如何使用C和互操作来编写程序的某些部分。 haskell为此目的还支持其他结构中的固定数组。

如果您需要的控制权超出了您的控制能力,那么haskell可能是错误的语言。从您的描述中无法判断出是否是您遇到的问题(您的要求似乎矛盾:您需要能够编写经过仔细的缓存调整的算法,但是任意GC暂停也可以吗?)。

最后一点:您不能依靠GHC的本机代码生成器来执行任何低级的强度降低优化,例如GCC会执行(GHC的NCG可能永远不会知道比特混乱,自动矢量化等)。相反,您可以尝试LLVM后端,但绝对不能保证程序中是否看到加速。