似乎Haskell试图成为一种安全的语言,并试图帮助程序员摆脱错误。例如,pred
/ succ
如果在外面会引发错误,div 1 0
也会抛出错误。这些安全的Haskell计算是什么,它们会导致什么开销?
是否可以关闭GHC的这种安全性,因为在无错误的程序中它们不是必需的?这会导致更好的速度性能吗?
对于C后端,有一个选项-ffast-math
。 LLVM后端或LLVM是否有任何此类性能选项?
答案 0 :(得分:25)
在此答案的前一版本中,基准确实存在严重缺陷。我道歉。
实际上,pred
,succ
和其他函数会在出现各种错误时引发异常,例如溢出和除零。正常的算术函数只是低级不安全函数的包装;例如,查看div
的{{1}}的实现:
Int32
您可以注意到在执行实际除法之前有两个检查!
然而,这些并不是最糟糕的。我们对数组进行了范围检查 - 有时会大大减慢代码的速度。传统上通过提供禁用检查的特殊功能变体(例如div x@(I32# x#) y@(I32# y#)
| y == 0 = divZeroError
| x == minBound && y == (-1) = overflowError
| otherwise = I32# (x# `divInt32#` y#)
)来解决这个特殊问题。
正如Daniel Fischer here所指出的那样,是一个解决方案,它允许您使用单个编译指示禁用/启用检查。不幸的是,它非常麻烦:你必须复制the source of GHC.Int并从每个函数中删除检查。当然,GHC.Int并不是此类功能的唯一来源。
如果您真的希望能够禁用支票,则必须:
unsafeAt
和import Prelude hiding (succ, pred, div, ...)
。然而,后一种变体不允许在安全和不安全功能之间进行简单的切换。假设存在一个已知不为零的数字(因此不需要检查)。现在,知道的人吗?要么是编译器,要么是你。在第一种情况下,我们可以期望编译器不执行任何检查。但在第二种情况下,我们的知识毫无用处 - 除非我们能以某种方式告诉编译器。所以,问题是:如何编码我们拥有的知识?这是一个众所周知的问题,有多种解决方案。显而易见的解决方案是让程序员明确地使用不安全的函数(import Unsafe (succ, pred, div, ...)
)。另一种解决方案是引入一些编译魔术:
unsafeRem
但我们的功能程序员有类型。我们习惯用类型编码信息。而且some of us很擅长。因此,最聪明的解决方案是引入一系列{-# ASSUME x/=0 #-}
gcd x y = ...
类型,或切换到依赖类型(即学习Agda)。
有关详细信息,请参阅non-empty lists。关注安全而非性能,但问题是相同的。
让我们尝试衡量安全和不安全之间的区别Unsafe
:
rem
差异似乎并不那么大:
{-# LANGUAGE MagicHash #-}
import GHC.Exts
import Criterion.Main
--assuming a >= b
--the type signatures are needed to prevent defaulting to Integer
safeGCD, unsafeGCD :: Int -> Int -> Int
safeGCD a b = if b == 0 then a else safeGCD b (rem a b)
unsafeGCD a b = if b == 0 then a else unsafeGCD b (unsafeRem a b)
{-# INLINE unsafeRem #-}
unsafeRem (I# a) (I# b) = I# (remInt# a b)
main = defaultMain [bench "safe" $ whnf (safeGCD 12452650) 11090050,
bench "unsafe" $ whnf (unsafeGCD 12452650) 11090050]
澄清正在添加的安全开销。
首先,如果安全措施可能导致异常,您可以了解它here。有一个列表可以抛出所有类型的异常。
程序员引发的异常(无人工开销):
$ ghc -O2 ../bench/bench.hs && ../bench/bench
benchmarking unsafe
mean: 215.8124 ns, lb 212.4020 ns, ub 220.1521 ns, ci 0.950
std dev: 19.71321 ns, lb 16.04204 ns, ub 23.83883 ns, ci 0.950
benchmarking safe
mean: 250.8196 ns, lb 246.7827 ns, ub 256.1225 ns, ci 0.950
std dev: 23.44088 ns, lb 19.06654 ns, ub 28.23992 ns, ci 0.950
:由ErrorCall
:error
:由AssertionFailed
。标准库引发的异常(重写库并且安全开销消失):
assert
:除零是其中之一。还包括溢出/下溢和一些不常见的。ArithException
:当索引超出范围或尝试引用未定义的元素时发生。ArrayException
:不要担心这些问题,与IO开销相比,开销很大。运行时异常(由GHC引起,不可避免):
IOException
:堆栈和堆溢出。只是轻微的开销。AsyncException
:没有开销(与PatternMatchFail
中的else
无法创建任何开销相同)。if...then...else...
:当您尝试处理记录的非existend字段时发生。由于必须执行字段存在的检查,因此会产生一些开销。Rec*Error
:没有开销。其次,如果存在不会导致异常的安全措施,我真的很想听听它(然后提交针对GHC的错误)。
通过by,NoMethodError
没有影响任何检查(它们是在Haskell代码中完成的,而不是在C中)。在某些边缘情况下,它只是以牺牲精度为代价来简化浮点运算。