Как HasCallStack влияет на производительность обычной ветки в Haskell?

Генерация стека вызовов при достижении ветки ошибок требует времени выполнения; это легко понять.

Но повлияет ли ограничение HasCallStack на производительность нормальной ветки? Как?

Ответ 1

Эффект добавления ограничения 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"

затем добавление HasCallStack к foo и bar (но оставляя 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

Таким образом, базовая, неоптимизированная стоимость производительности - это стоимость дополнительного параметра для каждой функции, украшенной HasCallStack, плюс стоимость выделения thunk для предоставления этого параметра для каждой точки вызова украшенной функции. (Эти расходы оплачиваются, даже если ошибка не возникает.)

На практике оптимизированный код будет... эээ... оптимизирован. Например, если приведенный выше пример скомпилирован с -O2, foo будет встроен, а bar будет специализироваться на определении baz таким образом, что единственная стоимость выполнения стека вызовов состоит в том, что статический указатель (на блок для создания полного стека вызовов для вызова error) передается в специализированную версию bar (но игнорируется, поскольку не генерируется ошибка).

GHC, кажется, не достаточно умен, чтобы определить, что baz никогда не последует примеру error и поэтому вообще не нуждается в кадре стека.