性能 – 编译不安全的Haskell - 代码日志

性能 – 编译不安全的Haskell

似乎Haskell试图成为一种安全的语言,并试图帮助程序员从错误。例如,如果外部使用了pred / succthrows错误,并且div 1 0也抛出。这些安全的Haskell计算是什么,它们造成什么开销?

是否可以关闭GHC的这种安全性,因为它们在无bug程序中不应该是必需的?那能造成更好的速度表现吗?

对于C后端,有一个选项-offast-math。 LLVM后端还是LLVM有这样的性能选项吗?

这个答案的前一版本确实存在严重缺陷。我道歉。

问题和解决方案,如果我们不要挖得太深

事实上,当出现各种错误时,例如溢出和除以零,pred,succ和其他函数引发异常。正常算术函数只是围绕低级不安全功能的包装器;作为一个例子,看看Int32的div的实现:

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

您可以注意到,在执行实际划分之前,有两个检查!

然而,这些并不是最差的。我们已经对数组进行范围检查 – 有时它会大大减慢代码。传统上通过提供禁用检查功能的特殊变体(如unsafeAt)来解决这个特殊问题。

正如Daniel Fischer here所指出的,它有一个解决方案,它允许您使用单个编译指示来禁用/启用检查。不幸的是,这是相当麻烦的:你必须复制the source of GHC.Int并从每个功能中删除支票。而GHC.Int当然不是这种功能的唯一来源。

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

写出你要使用的所有不安全的功能。
或者写一个包含重写规则的文件(如Daniel的帖子所述)并导入,或者只是导入前导隐藏(succ,pred,div,…)并导入Unsafe(succ,pred,div, ..)。然而,后一种变体不能简单地切换安全和不安全的功能。

问题的根源和实际解决方案的指针

假设有一个已知不为零的数字(因此不需要检查)。现在,谁知道了?要么编译,要么对你。在第一种情况下,我们可以期望编译器不执行任何检查,当然。但是在第二种情况下,我们的知识是无用的 – 除非我们可以以某种方式告诉编译器。那么问题是:如何编码知识呢?这是一个众所周知的问题,有多种解决方案。明显的解决方案是使程序员明确地使用不安全功能(unsafeRem)。另一个解决方案是介绍一些编译器的魔法:

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

但是我们功能的程序员有类型。我们习惯用类型编码信息。和some of us是伟大的。所以,最聪明的解决方案是引入一系列不安全类型,或切换到依赖类型(即学习Agda)。

有关更多信息,请阅读non-empty lists.关于安全性比性能更多,但问题是相同的。

没有那么糟糕

我们来试试衡量安全与不安全之间的区别:

{-# 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

了解你的敌人

澄清什么安全开销被添加。

首先,如果安全措施可能导致异常,您可以了解它here.有一个可以抛出的所有类型的异常的列表。

程序员造成的异常(无人工开销):

> ErrorCall:由错误引起:
> AssertionFailed:由断言引起的。

标准库抛出异常(重写库和安全开销)

> ArithException:除以零是其中之一。还涵盖溢出/下溢和一些较不常见的溢出。
> ArrayException:当索引超出范围或尝试引用未定义的元素时发生。
> IOException:不要担心这些,与IO开销相比,开销是惨淡的。

运行时异常(由GHC引起,不可避免):

> AsyncException:堆栈和堆溢出。只有很小的开销。
> PatternMatchFail:没有开销(与…相同的方式if … then … else …不创建任何)。
> Rec * Error:当您尝试解决记录的不存在的字段时发生。导致一些开销,因为必须执行字段的存在检查。
> NoMethodError:没有开销。
>关于并发的许多例外(死锁等):我不得不承认我没有看到他们的线索。

第二,如果存在不引起例外的安全措施,我真的很想听听(然后在GHC上提出错误)。

一句话

通过这一点,-ffast-math并不影响任何检查(它们是在Haskell代码中完成的,而不是C)。在某些边缘情况下,只是以牺牲精度为代价简单地进行浮点运算。

http://stackoverflow.com/questions/14429966/compile-unsafe-haskell

本站文章除注明转载外,均为本站原创或编译
转载请明显位置注明出处:性能 – 编译不安全的Haskell