为什么多态性在haskell(GHC)中如此昂贵?

时间:2013-09-17 16:17:01

标签: c++ haskell matrix monomorphism

我问这个问题,参考this SO问题。 Don Stewart接受的答案:第一行说“您的代码是高度多态的,将所有浮动变量改为Double ...”,它可以提高4倍的性能。

我有兴趣在Haskell中进行矩阵计算,我应该养成编写高度单态代码的习惯吗?

但是有些语言很好地利用了ad-hoc多态来生成快速代码,为什么GHC不会或不能呢? (读C ++或D)

为什么我们不能为Haskell提供像blitz ++或eigen这样的东西?我不明白GHC中的类型和(ad-hoc)多态性是如何工作的。

3 个答案:

答案 0 :(得分:18)

对于多态代码,通常需要在代码大小和代码速度之间进行权衡。要么为每个要操作的类型生成相同代码的单独版本,这会产生更大的代码,要么生成可以在多种类型上运行的单个版本,这将会更慢。

模板的C ++实现选择以增加代码大小为代价来提高代码速度。默认情况下,GHC采取相反的权衡。但是,可以使用SPECIALIZE和INLINABLE编译指示使GHC为不同类型生成单独的版本。这将导致多态代码具有类似于单态代码的速度。

答案 1 :(得分:18)

我想补充一下Dirk的回答,说INLINABLE通常建议超过SPECIALIZE。函数的INLINABLE注释保证模块导出函数的原始源代码,以便它可以在使用时专门化。这通常不需要为每个用例提供单独的SPECIALIZE编译指示。

INLINE不同,INLINABLE不会改变GHC的优化启发式。它只是说“请导出源代码”。

答案 2 :(得分:11)

  

我不明白类视网格在GHC中是如何工作的。

好的,请考虑这个功能:

linear :: Num x => x -> x -> x -> x
linear a b x = a*x + b

这需要三个数字作为输入,并返回一个数字作为输出。此函数接受任何数字类型;它是多态的。 GHC如何实现这一点?好吧,本质上编译器创建一个“类字典”,它包含其中的所有类方法(在这种情况下,+-*等。)这个字典成为一个额外的,隐藏函数的参数。像这样:

data NumDict x =
  NumDict
  {
    method_add :: x -> x -> x,
    method_subtract :: x -> x -> x,
    method_multiply :: x -> x -> x,
    ...
  }

linear :: NumDict x -> x -> x -> x -> x
linear dict a b x = a `method_multiply dict` x `method_add dict` b

每当你调用函数时,编译器会自动插入正确的字典 - 除非调用函数也是多态,在这种情况下它本身会收到一个字典,所以只是传递它。

事实上,缺乏多态性的函数通常更快,因为缺乏函数查找,但因为知道类型允许进行额外的优化。例如,我们的多态linear函数将适用于数字,向量,基质,比率,复数,任何。现在,如果编译器知道我们想要在Double上使用它,现在所有操作都成为单机器代码指令,所有操作数都可以在处理器寄存器中传递,依此类推。所有这些都会产生极其高效的代码。即使它是带有Double组件的复杂数字,我们也可以使它变得美观和高效。如果我们不知道我们会得到什么类型,我们就无法进行任何优化......这就是大多数速度差异通常来自的地方。


对于像线性这样的小函数,每次调用它时很可能会内联,导致没有多态性开销和少量代码重复 - 就像C ++模板一样。对于更大,更复杂的多态函数,可能会有一些成本。一般情况下,编译器决定这一点,而不是你 - 除非你想开始在这个地方撒上pragma。 ;-)或者,如果你实际上没有使用任何多态,你可以只给出所有单形类型的签名......