在Haskell中定义类型类的实例,您需要提供类型类所需的函数字典。即要定义Bounded
的实例,您需要提供minBound
和maxBound
的定义。
出于这个问题的目的,让我们将这个字典称为类型类实例的vtbl
。如果这不是类比,请告诉我。
我的问题在于,当我调用类型类函数时,我可以从GHC期望什么样的代码生成。在这种情况下,我看到三种可能性:
我想了解每一次发生的时间 - 或者是否有其他可能性。
此外,如果类型类是在单独编译的模块中定义而不是作为“主”编译单元的一部分,这是否重要?
在一个可运行的程序中,似乎Haskell知道程序中所有函数和表达式的类型。因此,当我调用类型类函数时,编译器应该知道vtbl是什么以及确切调用哪个实现函数。我希望编译器至少生成对实现函数的直接调用。这是真的吗?
(我在这里说“runnable program”,以区别于编译你没有运行的模块。)
答案 0 :(得分:20)
与所有好问题一样,答案是“它取决于”。经验法则是任何类型类多态代码都有运行时成本。但是,库作者在使用GHC的重写规则消除这种成本方面具有很大的灵活性,特别是有{-# SPECIALIZE #-}
pragma可以自动创建多态函数的单态版本,并且只要可以推断出多态函数,就可以使用它们。用于单形类型。 (我认为这样做的代价是库和可执行文件的大小。)
您可以使用ghc的-ddump-simpl
标记回答任何特定代码段的问题。例如,这是一个简短的Haskell文件:
vDouble :: Double
vDouble = 3
vInt = length [2..5]
main = print (vDouble + realToFrac vInt)
如果没有优化,您可以看到GHC在运行时执行字典查找:
Main.main :: GHC.Types.IO ()
[GblId]
Main.main =
System.IO.print
@ GHC.Types.Double
GHC.Float.$fShowDouble
(GHC.Num.+
@ GHC.Types.Double
GHC.Float.$fNumDouble
(GHC.Types.D# 3.0)
(GHC.Real.realToFrac
@ GHC.Types.Int
@ GHC.Types.Double
GHC.Real.$fRealInt
GHC.Float.$fFractionalDouble
(GHC.List.length
@ GHC.Integer.Type.Integer
(GHC.Enum.enumFromTo
@ GHC.Integer.Type.Integer
GHC.Enum.$fEnumInteger
(__integer 2)
(__integer 5)))))
......相关位为realToFrac @Int @Double
。另一方面,在-O2
,您可以看到它静态执行字典查找并内联实现,结果是对int2Double#
的单次调用:
Main.main2 =
case GHC.List.$wlen @ GHC.Integer.Type.Integer Main.main3 0
of ww_a1Oq { __DEFAULT ->
GHC.Float.$w$sshowSignedFloat
GHC.Float.$fShowDouble_$sshowFloat
GHC.Show.shows26
(GHC.Prim.+## 3.0 (GHC.Prim.int2Double# ww_a1Oq))
(GHC.Types.[] @ GHC.Types.Char)
}
图书馆作者也可以选择将多态函数重写为对单态函数的调用,但不能内联单态函数的实现;这意味着所有你提出的可能性(以及更多)是可能的。
答案 1 :(得分:11)
如果编译器可以在编译时“告诉”您正在使用的实际类型,那么方法查找在编译时发生。否则它会在运行时发生。如果在编译时进行查找,则方法代码可以内联,具体取决于方法的大小。 (这适用于常规函数:如果编译器知道您正在调用哪个函数,那么如果该函数“足够小”,它将内联它。)
例如,考虑(sum [1 .. 10]) :: Integer
。在这里,编译器静态地知道列表是Integer
拍摄的列表,因此它可以内联+
的{{1}}函数。另一方面,如果您执行类似
Integer
然后,当你调用foo :: Num x => [x] -> x
foo xs = sum xs - head x
时,编译器不知道你正在使用什么类型。 (这取决于给sum
的类型),因此它不能进行任何编译时查找。
另一方面,使用foo
编译指示,您可以执行类似
{-# SPECIALIZE #-}
这样做是告诉编译器编译{-# SPECIALIZE foo:: [Int] -> Int #-}
的特殊版本,其中输入是foo
值的列表。这显然意味着该版本的 ,编译器可以在编译时进行所有方法查找(几乎可以肯定地将它们全部内联)。现在有两个版本的Int
- 一个适用于任何类型并运行时类型查找,一个仅适用于foo
,但[可能]更快。
当您调用Int
函数时,编译器必须决定调用哪个版本。如果编译器可以在编译时“告诉”你想要foo
版本,它就会这样做。如果它无法“告诉”你将使用什么类型,它将使用较慢的任何类型版本。
请注意,您可以拥有单个函数的多个特化。例如,你可以做
Int
现在,只要编译器可以告诉您正在使用其中一种类型,它就会使用该类型的硬编码版本。但是如果编译器无法分辨你正在使用什么类型,它将永远不会使用专用版本,并且永远不会使用多态版本。 (这可能意味着您需要专门调用调用{-# SPECIALIZE foo :: [Int] -> Int #-}
{-# SPECIALIZE foo :: [Double] -> Double #-}
{-# SPECIALIZE foo :: [Complex Double] -> Complex Double #-}
的函数。)
如果你在编译器的Core输出周围爬行,你可能会弄清楚它在任何特定情况下的确切行为。你可能会疯狂地疯狂......但
答案 2 :(得分:9)
正如其他答案所述,任何这些都可能在不同的情况下发生。对于任何特定的函数调用,唯一可以确定的方法是查看生成的核心。也就是说,在某些情况下,您可以很好地了解会发生什么。
在单态类型中使用类型类方法。
当在编译时完全知道类型的情况下调用类型类方法时,GHC将在编译时执行查找。例如
isFive :: Int -> Bool
isFive i = i == 5
这里编译器知道它需要Int
的Eq字典,因此它会发出代码来静态调用该函数。是否内联该调用取决于GHC通常的内联规则,以及INLINE
pragma是否适用于类方法定义。
公开多态函数
如果从编译模块中公开了多态函数,那么基本情况是需要在运行时执行查找。
module Foo (isFiveP) where
isFiveP :: (Eq a, Num a) => a -> Bool
isFiveP i = i == 5
GHC实际上做的是将其转换为形式的函数(或多或少)
isFiveP_ eqDict numDict i = (eq_op eqDict) i (fromIntegral_fn numDict 5)
所以函数查找需要在运行时执行。
无论如何,这是基本情况。实际发生的事情是GHC对跨模块内联非常积极。 isFiveP
足够小,可以内联到呼叫网站。如果可以在调用站点确定类型,则字典查找将全部在编译时执行。即使在调用站点没有直接内联多态函数,由于GHC通常的函数转换,字典查找仍然可以在编译时执行,如果代码到达一个函数(带有类字典参数)可以的形式适用于一个静态知名词典。