HasCallStack如何影响Haskell中普通分支的性能?

luo*_*990 12 performance haskell

到达错误分支时生成调用堆栈会花费运行时间;这很容易理解。

但是HasCallStack约束还会影响正常分支的性能吗?怎么样?

K. *_*uhr 7

HasCallStack向函数添加约束的效果大致foo等同于:

  • 为调用堆栈添加一个附加的输入参数到foo的参数列表中;
  • 无论在何处foo调用,都通过将信息框(由函数名“ foo”和被调用的源位置组成)压入输入调用栈(如果foo从具有HasCallStack约束的另一个函数中调用)来构造调用栈参数)或到一个空的调用栈中(如果从没有HasCallStack约束的函数中调用它)。

所以...如果您有一些功能:

foo :: HasCallStack => Int -> String -> String
foo n = bar n '*'

bar :: HasCallStack => Int -> Char -> String -> String
bar n c str = if n >= 0 then c' ++ ' ':str ++ ' ':c'
              else error "bad n"
  where c' = replicate n c

baz :: String
baz = foo 3 "hello"
Run Code Online (Sandbox Code Playgroud)

然后添加HasCallStackfoobar(但不理会baz)基本上具有与您编写的效果相同的效果:

foo cs n = bar cs' n
  where cs' = pushCallStack ("bar", <loc>) cs
bar cs n c str
  = if n >= 0 then c' ++ ' ':str ++ ' ':c'
    else error cs' "bad n"
  where c' = replicate n c
        cs' = pushCallStack ("error", <loc>) cs
baz = foo cs' 3 "hello"
  where cs' = pushCallStack ("foo", <loc>) emptyCallStack
Run Code Online (Sandbox Code Playgroud)

因此,基准的,未优化的性能成本是每个装饰有功能的参数的额外成本,HasCallStack再加上为装饰后的功能的每个调用点提供该参数的重分配的成本。(即使未触发任何错误,也要支付这些费用。)

在实践中,优化后的代码将被……优化。例如,如果上面的示例使用编译-O2foo将被内联并且bar将专门用于的定义,baz以使调用堆栈的唯一运行时开销是静态指针(指向用于创建完整调用堆栈的thunk) (用于error调用)传递给的专用版本bar(但会被忽略,因为不会产生错误)。

GHC似乎还不够聪明,无法确定这种baz情况永远不会发生error,因此根本不需要堆栈框架。