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不是这些功能的唯一来源.
如果您真的希望能够禁用支票,则必须:
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:没有开销(以同样的方式else在if...then...else...不产生任何).Rec*Error:当您尝试处理记录的非existend字段时发生.由于必须执行字段存在的检查,因此会产生一些开销.NoMethodError:没有开销.其次,如果存在不会导致异常的安全措施,我真的很想听听它(然后提交针对GHC的错误).
通过by,-ffast-math没有影响任何检查(它们是在Haskell代码中完成的,而不是在C中).在某些边缘情况下,它只是以牺牲精度为代价来简化浮点运算.