20

Haskellは安全な言語を目指しており、プログラマーが間違いを犯さないように手助けしようとしているようです。たとえば、pred/succは外部の場合はエラーをスローし、div 1 0またスローします。これらの安全なHaskell計算とは何ですか、またそれらはどのようなオーバーヘッドを引き起こしますか?

バグのないプログラムでは必要ないはずなので、GHCのこの種の安全性をオフにすることは可能ですか?そして、それはより良い速度性能を引き起こすことができますか?

Cバックエンドには、オプションがありました-ffast-math。LLVMバックエンドまたはLLVMにそのようなパフォーマンスオプションはありますか?

4

1 に答える 1

25

ベンチマークは、この回答の以前のバージョンでは確かに深刻な欠陥がありました。謝罪します。

深く掘り下げなければ、問題と解決策

実際、、predおよびsuccその他の関数は、オーバーフローやゼロ除算などのさまざまなエラーが発生したときに例外を発生させます。通常の算術関数は、低レベルの安全でない関数の単なるラッパーです。div例として、 for Int32:の実装を見てください。

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

実際の除算が実行される前に、 2つのチェックがあることに気付くでしょう!

ただし、これらは最悪のものではありません。配列の境界範囲チェックがあります—コードの速度が大幅に低下する場合があります。この特定の問題は、従来、チェックが無効になっている関数の特殊なバリアント(などunsafeAt)を提供することで解決されていました。

ここでDanielFischer指摘しているように、単一のプラグマでチェックを無効/有効にできるソリューションがあります。残念ながら、これは非常に面倒です。GHC.Intのソースをコピーして、すべての関数からチェックを切り取る必要があります。もちろん、GHC.Intだけがそのような関数のソースではありません。

本当にチェックを無効にできるようにしたい場合は、次のことを行う必要があります。

  1. 使用する安全でない関数をすべて記述してください。
  2. (Danielの投稿で説明されているように)書き換えルールを含むファイルを書き込んでインポートするか、単に実行import Prelude hiding (succ, pred, div, ...)してimport Unsafe (succ, pred, div, ...)。ただし、後者のバリアントでは、安全な機能と安全でない機能を簡単に切り替えることはできません。

問題の根本と実際の解決策へのポインタ

ゼロではないことがわかっている数値があるとします(したがって、チェックは必要ありません)。さて、それはに知られていますか?コンパイラー、またはあなたのどちらかに。もちろん、最初のケースでは、コンパイラがチェックを実行しないことが期待できます。しかし、2番目のケースでは、私たちの知識は役に立ちません—どういうわけかコンパイラにそれについて伝えることができない限り。したがって、問題は、私たちが持っている知識をどのようにエンコードするかということです。そして、これはよく知られた問題であり、複数の解決策があります。明らかな解決策は、プログラマーに安全でない関数(unsafeRem)を明示的に使用させることです。別の解決策は、コンパイラの魔法を導入することです。

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

しかし、私たち機能プログラマーにはタイプがあります。そして、私たちは情報を型でエンコードすることに慣れています。そして、私たちの何人かはそれが得意です。したがって、最も賢明な解決策は、型のファミリーを導入するか、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]

違いはそれほど大きくはないようです。

$ 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:ゼロ除算はその1つです。オーバーフロー/アンダーフローといくつかのあまり一般的でないものもカバーしています。
  • ArrayException:インデックスが範囲外の場合、または未定義の要素を参照しようとした場合に発生します。
  • IOException:それらについて心配する必要はありません。IOオーバーヘッドと比較してオーバーヘッドは暗いです。

ランタイム例外(GHCによって引き起こされ、避けられない):

  • AsyncException:スタックとヒープがオーバーフローします。わずかなオーバーヘッドのみ。
  • PatternMatchFail:オーバーヘッドなし(inが何も作成しないのと同じ方法elseif...then...else...
  • Rec*Error:レコードの存在しないフィールドをアドレス指定しようとしたときに発生します。フィールドの存在のチェックを実行する必要があるため、オーバーヘッドが発生します。
  • NoMethodError:オーバーヘッドなし。
  • 並行性に関する多数の例外(デッドロックなど):私はそれらについての手がかりを持っていないことを告白しなければなりません。

第二に、例外を引き起こさない安全対策が存在する場合、私はそれについて本当に聞きたいです(そしてGHCに対してバグを提出します)。

備考

byまでに、-ffast-mathチェックには影響していませんでした(CではなくHaskellコードで実行されました)。一部のエッジケースでは精度を犠牲にして、浮動小数点演算を高速化するだけでした。

于 2013-01-20T22:43:00.130 に答える