编译不安全的Haskell

tel*_*one 20 performance haskell llvm ghc

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

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

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

Art*_*yom 25

该答案的前一版本确实存在严重缺陷.我道歉.

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

实际上,当各种错误发生时pred,succ和其他函数会引发异常,例如溢出和除零.正常的算术函数只是低级不安全函数的包装; 作为一个例子,看看divfor 的实现Int32:

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

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

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

正如丹尼尔·菲舍尔指出,这里,还有就是它不会让你禁用/启用一个单一的编译检查的解决方案.不幸的是,它非常麻烦:你必须复制GHC.Int的来源并从每个函数中删除检查.当然,GHC.Int不是这些功能的唯一来源.

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

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

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

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

{-# ASSUME x/=0 #-}
gcd x y = ...
Run Code Online (Sandbox Code Playgroud)

但我们的功能程序员有类型.我们习惯用类型编码信息.而我们中的一些是在它巨大的.因此,最智能的解决方案是引入一系列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]
Run Code Online (Sandbox Code Playgroud)

差异似乎并不那么大:

$ 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
Run Code Online (Sandbox Code Playgroud)

了解你的敌人

澄清了正在添加的安全开销.

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

程序员引起的异常(无人工开销):

  • ErrorCall:由error:引起:
  • AssertionFailed:引起的assert.

标准库引发的异常(重写库和安全开销消失):

  • ArithException:除零是其中之一.还包括溢出/下溢和一些不太常见的.
  • ArrayException:当索引超出范围或尝试引用未定义的元素时发生.
  • IOException:不用担心,与IO开销相比,开销很惨淡.

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

  • AsyncException:堆栈和堆溢出.只是轻微的开销.
  • PatternMatchFail:没有开销(以同样的方式elseif...then...else...不产生任何).
  • Rec*Error:当您尝试处理记录的非existend字段时发生.由于必须执行字段存在的检查,因此会产生一些开销.
  • NoMethodError:没有开销.
  • 关于并发(死锁等)的大量例外:我必须承认我对它们一无所知.

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

一句话

通过by,-ffast-math没有影响任何检查(它们是在Haskell代码中完成的,而不是在C中).在某些边缘情况下,它只是以牺牲精度为代价来简化浮点运算.

  • @telephone加速慢速正确程序比修复快速错误程序更容易.当它在"足够快"和"不够快"之间产生差异时禁用安全检查比在决定性能无关紧要时手动打开安全检查更有意义.如果你一开始就知道项目中的每一段代码都需要最好的性能(这是非常罕见的情况),不要在Haskell中编写代码,用C语言编写. (9认同)