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
(暂时忽略 Fact
和 Factored
)。我在不同级别对代码进行基准测试,比较 foo
、t
和 bar
的性能。在基准测试中,t = foo
和 bar
仅通过 newtype
应用 t
。因此,它们的 运行 时间应该基本相同,但是 criterion 报告说 foo
需要 9.2ns,t
大约是 17.45ns 的两倍,而 bar
需要高达 268.1ns。
我已经尝试添加 INLINABLE
甚至 SPECIALIZE
编译指示,但它们没有帮助。我想相信 GHC 有一些魔力 syntax/optimization/etc 可以 一致地 应用于解决这些类型的性能问题。例如,我见过以无点风格编写代码对性能有显着影响的案例。
可以找到完整的代码here。我保证这不是吓人的。这些模块是:
- Foo:定义
Foo
、foo
和 T
- 栏:定义
Bar
和 bar
- FooBench:定义了
foo
的基准
- TBench:定义
t
的基准
- BarBench:定义了
bar
的基准
- 主要:运行三个基准
- 因式分解:使用 singletons
定义 Fact
和 Factored
大部分模块都很小;我在单独的文件中定义了三个基准,以便我可以检查它们的核心。我为三个 *Bench
模块生成了核心,并尽我所能对齐它们。它们每行只有 ~250 行,前 200 行是相同的,直到重命名。问题是我不知道最后 50 行左右的内容。 FooBench
与 TBench
的(核心)差异是 here, the diff for TBench
vs BarBench
is here, and the diff for FooBench
vs BarBench
is here.
我有几个问题:
在较高的层次上,核心文件之间的本质区别是什么?我正在寻找类似“这里 你可以看到 GHC 没有内联对 x
的调用。”我应该寻找什么?
怎样做才能让三个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
相同。 (事实上 TBench
和 BarBench
的核心是完全一样的,除了类型签名之外)。
我不完全确定为什么,但是使用 INLINE
而不是 INLINEABLE
会使 t
和 bar
与 foo
执行相同的操作。我不是专家,但通常最好将 INLINE
用于您确定要内联的小函数。
也就是说,我认为其中一些问题来自基准如何阻止 ghc
作弊。例如,在您的原始代码上编写 bench "Bar" $ nf (GHC.Magic.inline bar) x
会使 bar
执行与 foo
相同的操作。我怀疑 "real" 程序不会那么精致。
我的代码中有这些基本类型:
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
(暂时忽略 Fact
和 Factored
)。我在不同级别对代码进行基准测试,比较 foo
、t
和 bar
的性能。在基准测试中,t = foo
和 bar
仅通过 newtype
应用 t
。因此,它们的 运行 时间应该基本相同,但是 criterion 报告说 foo
需要 9.2ns,t
大约是 17.45ns 的两倍,而 bar
需要高达 268.1ns。
我已经尝试添加 INLINABLE
甚至 SPECIALIZE
编译指示,但它们没有帮助。我想相信 GHC 有一些魔力 syntax/optimization/etc 可以 一致地 应用于解决这些类型的性能问题。例如,我见过以无点风格编写代码对性能有显着影响的案例。
可以找到完整的代码here。我保证这不是吓人的。这些模块是:
- Foo:定义
Foo
、foo
和T
- 栏:定义
Bar
和bar
- FooBench:定义了
foo
的基准
- TBench:定义
t
的基准
- BarBench:定义了
bar
的基准
- 主要:运行三个基准
- 因式分解:使用 singletons 定义
Fact
和 Factored
大部分模块都很小;我在单独的文件中定义了三个基准,以便我可以检查它们的核心。我为三个 *Bench
模块生成了核心,并尽我所能对齐它们。它们每行只有 ~250 行,前 200 行是相同的,直到重命名。问题是我不知道最后 50 行左右的内容。 FooBench
与 TBench
的(核心)差异是 here, the diff for TBench
vs BarBench
is here, and the diff for FooBench
vs BarBench
is here.
我有几个问题:
在较高的层次上,核心文件之间的本质区别是什么?我正在寻找类似“这里 你可以看到 GHC 没有内联对
x
的调用。”我应该寻找什么?怎样做才能让三个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
相同。 (事实上 TBench
和 BarBench
的核心是完全一样的,除了类型签名之外)。
我不完全确定为什么,但是使用 INLINE
而不是 INLINEABLE
会使 t
和 bar
与 foo
执行相同的操作。我不是专家,但通常最好将 INLINE
用于您确定要内联的小函数。
也就是说,我认为其中一些问题来自基准如何阻止 ghc
作弊。例如,在您的原始代码上编写 bench "Bar" $ nf (GHC.Magic.inline bar) x
会使 bar
执行与 foo
相同的操作。我怀疑 "real" 程序不会那么精致。