Haskell 以身作则

Haskell Performance by Example

我的代码中有这些基本类型:

newtype Foo (m :: Factored) = Foo Int64 deriving (NFData)

foo :: forall m . (Fact m) => Foo m -> Foo m

class T t where t :: (Fact m ) => t m -> t m

instance T Foo where t = foo

newtype Bar t (m :: Factored) = Bar (t m) deriving (NFData)

bar :: (Fact m, T t) => Bar t m -> Bar t m
bar (Bar v) = Bar $ t v

(暂时忽略 FactFactored)。我在不同级别对代码进行基准测试,比较 footbar 的性能。在基准测试中,t = foobar 仅通过 newtype 应用 t。因此,它们的 运行 时间应该基本相同,但是 criterion 报告说 foo 需要 9.2ns,t 大约是 17.45ns 的两倍,而 bar 需要高达 268.1ns。

我已经尝试添加 INLINABLE 甚至 SPECIALIZE 编译指示,但它们没有帮助。我想相信 GHC 有一些魔力 syntax/optimization/etc 可以 一致地 应用于解决这些类型的性能问题。例如,我见过以无点风格编写代码对性能有显着影响的案例。

可以找到完整的代码here。我保证这不是吓人的。这些模块是:

大部分模块都很小;我在单独的文件中定义了三个基准,以便我可以检查它们的核心。我为三个 *Bench 模块生成了核心,并尽我所能对齐它们。它们每行只有 ~250 行,前 200 行是相同的,直到重命名。问题是我不知道最后 50 行左右的内容。 FooBenchTBench 的(核心)差异是 here, the diff for TBench vs BarBench is here, and the diff for FooBench vs BarBench is here.

我有几个问题:

  1. 在较高的层次上,核心文件之间的本质区别是什么?我正在寻找类似“这里 你可以看到 GHC 没有内联对 x 的调用。”我应该寻找什么?

  2. 怎样做才能让三个benchmarks在9.2ns左右全部运行? GHC 优化? INLINE/INLINABLE 编译指示? SPECIALIZE 我错过的编译指示? (您不允许专门针对 F128::Factored;在我的真实库中,此值可能会在 运行 时具体化。)Restricting/delaying 内联到特定阶段?

虽然我正在寻找一个实际的解决方案来使基准测试全部快速,但适用于此示例的技巧可能无法扩展到我的真实库。因此,我也在寻找 "high-level" 对特定技术为何有效的解释。

首先,查看bar

bar :: (Fact m, T t) => Bar t m -> Bar t m
bar (Bar v) = Bar $ t v

我们可以在不需要参数的情况下使用 coerce:

bar :: (Fact m, T t) => Bar t m -> Bar t m
bar = (coerce :: (t m -> t m) -> Bar t m -> Bar t m) t

这(正如我们希望的那样)使 bar 的性能与 t 相同。 (事实上​​ TBenchBarBench 的核心是完全一样的,除了类型签名之外)。

我不完全确定为什么,但是使用 INLINE 而不是 INLINEABLE 会使 tbarfoo 执行相同的操作。我不是专家,但通常最好将 INLINE 用于您确定要内联的小函数。

也就是说,我认为其中一些问题来自基准如何阻止 ghc 作弊。例如,在您的原始代码上编写 bench "Bar" $ nf (GHC.Magic.inline bar) x 会使 bar 执行与 foo 相同的操作。我怀疑 "real" 程序不会那么精致。