似乎Haskell试图成为一种安全的语言,并试图帮助程序员避免错误。例如,pred
/succ
在越界时会抛出错误,div 1 0
也会抛出错误。这些安全的Haskell计算是什么,它们会带来什么开销?
是否可以关闭GHC中的这种安全性,因为在没有缺陷的程序中它们不应该是必要的?那样能够提高运行速度的性能吗?
对于C后端,有一个选项-ffast-math
。LLVM后端或LLVM有类似的性能选项吗?
似乎Haskell试图成为一种安全的语言,并试图帮助程序员避免错误。例如,pred
/succ
在越界时会抛出错误,div 1 0
也会抛出错误。这些安全的Haskell计算是什么,它们会带来什么开销?
是否可以关闭GHC中的这种安全性,因为在没有缺陷的程序中它们不应该是必要的?那样能够提高运行速度的性能吗?
对于C后端,有一个选项-ffast-math
。LLVM后端或LLVM有类似的性能选项吗?
div x@(I32# x#) y@(I32# y#)
| y == 0 = divZeroError
| x == minBound && y == (-1) = overflowError
| otherwise = I32# (x# `divInt32#` y#)
unsafeAt
)。import Prelude hiding (succ, pred, div, ...)
和 import Unsafe (succ, pred, div, ...)
。后一种变体不允许简单地在安全和不安全函数之间切换。假设有一个已知不为零的数字(因此不需要检查)。现在,这个信息是由谁知道的?是编译器还是你?如果是前者,我们当然可以期望编译器不会执行任何检查。但在后一种情况下,我们的知识是无用的——除非我们能以某种方式告诉编译器。因此,问题是:如何编码我们拥有的知识?这是一个众所周知的问题,有多种解决方案。显而易见的解决方案是让程序员显式地使用不安全函数(unsafeRem
)。另一种解决方案是引入一些编译器魔法:
{-# ASSUME x/=0 #-}
gcd x y = ...
但是,我们函数式程序员有{类型}。我们习惯于使用类型编码信息。其中一些人擅长此类操作。因此,最明智的解决方案要么是引入一组Unsafe
类型,要么是切换到依赖类型(即学习Agda)。
有关非空列表的更多信息,请阅读相关资料。那里的问题更多地关注安全性而不是性能,但问题是相同的。
让我们尝试衡量安全和不安全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]
这个差别似乎并不是那么大:
$ 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
:当索引超出范围或尝试引用未定义元素时会发生。IOException
:不用担心这些,与IO开销相比,开销微不足道。运行时异常(由GHC引起,无法避免):
AsyncException
:堆栈和堆溢出。仅有轻微的开销。PatternMatchFail
:没有开销(与if...then...else...
中的else
一样)。Rec*Error
:当您尝试访问记录的不存在字段时发生。需要执行字段存在性检查,因此会产生一些开销。NoMethodError
:没有开销。其次,如果存在不会引起异常的安全措施,我真的很想听听(然后针对GHC提交错误报告)。
顺便说一下,-ffast-math
并没有影响任何检查(它们是在Haskell代码中完成的,而不是在C代码中)。它只是以某些边缘情况的精度为代价,使浮点运算更快。
safeGCD
已经默认为 Integer -> Integer -> Integer
... - Artyom