20

似乎 Haskell 试图成为一种安全的语言,并试图帮助程序员避免错误。例如,如果在外面, pred/succ会抛出错误,div 1 0也会抛出。这些安全的 Haskell 计算是什么,它们会导致什么开销?

是否可以关闭 GHC 的这种安全性,因为在无错误的程序中它们不应该是必需的?这能带来更好的速度性能吗?

对于 C 后端,有一个选项-ffast-math. LLVM 后端或 LLVM 是否有任何此类性能选项?

4

1 回答 1

25

该基准在此答案的先前版本中确实存在严重缺陷。我道歉。

问题和解决方案,如果我们不深入挖掘

事实上,pred,succ和其他函数在发生各种错误时引发异常,例如溢出和被零除。普通算术函数只是低级不安全函数的包装;例如,看一下 for 的div实现Int32

div     x@(I32# x#) y@(I32# y#)
    | y == 0                     = divZeroError
    | x == minBound && y == (-1) = overflowError
    | otherwise                  = I32# (x# `divInt32#` y#)

您可以注意到在执行实际除法之前有两次检查!

然而,这些还不是最糟糕的。我们对数组进行了边界范围检查——有时它会大大减慢代码的速度。传统上,这个特定问题是通过提供禁用检查的特殊函数变体来解决的(例如unsafeAt)。

正如 Daniel Fischer在这里指出的那样,有一个解决方案可以让您使用单个编译指示禁用/启用检查。不幸的是,这很麻烦:您必须复制GHC.Int 的源代码并删除每个函数的检查。当然,GHC.Int 并不是此类函数的唯一来源。

如果您真的希望能够禁用检查​​,则必须:

  1. 编写所有你将要使用的不安全函数。
  2. 要么编写一个包含重写规则的文件(如 Daniel 的帖子中所述)并导入它,要么只执行import Prelude hiding (succ, pred, div, ...)and import Unsafe (succ, pred, div, ...). 但是,后一种变体不允许在安全和不安全功能之间进行简单的切换。

问题的根源和真正解决方案的指针

假设有一个已知不为零的数字(因此不需要检查)。现在,知道它?要么给编译器,要么给你。在第一种情况下,我们当然可以期望编译器不执行任何检查。但在第二种情况下,我们的知识毫无用处——除非我们能以某种方式告诉编译器。所以,问题是:如何编码我们拥有的知识?这是一个众所周知的问题,有多种解决方案。显而易见的解决方案是让程序员显式地使用不安全函数(unsafeRem)。另一个解决方案是引入一些编译器魔法:

{-# ASSUME x/=0 #-}
gcd x y = ...

但是我们函数式程序员有类型。而且我们习惯于使用类型对信息进行编码。我们中的一些人很擅长。因此,最聪明的解决方案是引入一系列Unsafe类型,或者切换到依赖类型(即学习 Agda)。

有关更多信息,请阅读非空列表。与性能相比,人们对安全性的关注更多,但问题是一样的。

还不错

让我们尝试衡量 safe 和 unafe 之间的区别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: 没有开销(就像elseinif...then...else...不创建任何开销一样)。
  • Rec*Error:当您尝试处理记录的不存在字段时发生。由于必须检查字段是否存在,因此会导致一些开销。
  • NoMethodError: 没有开销。
  • 许多关于并发的异常(死锁等):我不得不承认我对它们一无所知。

其次,如果存在不会导致异常的安全措施,我真的很想听听它(然后针对 GHC 提交错误)。

一句话

到目前为止,-ffast-math并没有影响任何检查(它们是在 Haskell 代码中完成的,而不是在 C 中)。它只是在某些边缘情况下以牺牲精度为代价使浮点运算更快。

于 2013-01-20T22:43:00.130 回答