编译不安全的Haskell

时间:2013-01-20 22:17:27

标签: performance haskell llvm ghc

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

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

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

1 个答案:

答案 0 :(得分:25)

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

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

实际上,predsucc和其他函数会在出现各种错误时引发异常,例如溢出和除零。正常的算术函数只是低级不安全函数的包装;例如,查看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并不是此类功能的唯一来源。

如果您真的希望能够禁用支票,则必须:

  1. 写下您将要使用的所有不安全功能。
  2. 编写一个包含重写规则的文件(如Daniel的帖子中所述)并导入它,或者只执行unsafeAtimport Prelude hiding (succ, pred, div, ...)。然而,后一种变体不允许在安全和不安全功能之间进行简单的切换。
  3. 问题的根源和指向真实解决方案的指针

    假设存在一个已知不为零的数字(因此不需要检查)。现在,知道的人吗?要么是编译器,要么是你。在第一种情况下,我们可以期望编译器不执行任何检查。但在第二种情况下,我们的知识毫无用处 - 除非我们能以某种方式告诉编译器。所以,问题是:如何编码我们拥有的知识?这是一个众所周知的问题,有多种解决方案。显而易见的解决方案是让程序员明确地使用不安全的函数(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中)。在某些边缘情况下,它只是以牺牲精度为代价来简化浮点运算。