Ответ 1
В предыдущей версии этого ответа эта контрольная точка была действительно ошибочной. Прошу прощения.
Проблема и решение, если мы не выкапываем слишком глубоко
Действительно, pred
, succ
и другие функции вызывают исключения при возникновении различных ошибок, таких как переполнение и деление на ноль. Обычные арифметические функции - это просто обертки вокруг низкоуровневых небезопасных функций; в качестве примера рассмотрим реализацию div
для Int32
:
div [email protected](I32# x#) [email protected](I32# y#)
| y == 0 = divZeroError
| x == minBound && y == (-1) = overflowError
| otherwise = I32# (x# `divInt32#` y#)
Вы можете заметить, что перед фактическим делением выполняется две проверки!
Тем не менее, эти самые худшие. У нас есть диапазон проверки диапазона для массивов - иногда это сильно замедляет код. Эта конкретная проблема традиционно решается путем предоставления специальных вариантов функций с отключенными проверками (например,unsafeAt
).
Как отметил Даниэль Фишер здесь, есть решение, которое позволяет вам отключать/включать проверки с помощью одной прагмы. К сожалению, это довольно громоздко: вам придется скопировать источник GHC.Int и вырезать чеки от каждой функции. И GHC.Int не является единственным источником таких функций, конечно.
Если вы действительно хотите отключить проверки, вы должны:
- Напишите все небезопасные функции, которые вы собираетесь использовать.
- Либо напишите файл, который будет содержать правила перезаписи (как описано в сообщении Daniels), и импортируйте его, либо просто
import Prelude hiding (succ, pred, div, ...)
иimport Unsafe (succ, pred, div, ...)
. Последний вариант не позволит просто переключаться между безопасными и небезопасными функциями.
Корень проблемы и указатели на реальные решения
Предположим, что существует число, которое, как известно, не равно нулю (следовательно, требуется наличие проверок arent). Теперь, кому это известно? Либо компилятору, либо вам. В первом случае мы можем ожидать, что компилятор не выполнит никаких проверок, конечно. Но во втором случае наши знания бесполезны - если мы не можем как-то рассказать об этом компилятору. Итак, проблема заключается в следующем: как кодировать полученные знания? И это хорошо известная проблема с несколькими решениями. Очевидное решение состоит в том, чтобы заставить программиста использовать небезопасную функцию (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
: деление на ноль является одним из них. Также охватывает переполнение/переполнение и некоторые менее распространенные. -
ArrayException
: происходит, когда индекс выходит за пределы или когда вы пытаетесь ссылаться на неопределенный элемент. -
IOException
: не беспокойтесь о них, накладные расходы мрачны по сравнению с издержками ввода-вывода.
Исключения времени выполнения (вызванные GHC, неизбежные):
-
AsyncException
: переполнение стека и кучи. Только незначительные накладные расходы. -
PatternMatchFail
: нет служебных данных (так же, какelse
вif...then...else...
не создает каких-либо). -
Rec*Error
: происходит, когда вы пытаетесь обратиться к не существующему полю записи. Вызывает некоторые накладные расходы, так как необходимо проверить существование полей. -
NoMethodError
: нет накладных расходов. - множество исключений относительно concurrency (тупиков и т.д.): я должен признаться, что я не знал о них.
Во-вторых, если существует мера безопасности, которая не вызывает исключения, мне бы очень хотелось услышать об этом (а затем подать сообщение об ошибке GHC).
Замечание
By by, -ffast-math
не влиял на какие-либо проверки (они были выполнены в коде Haskell, а не на C). Это просто делало операции с плавающей запятой быстрее за счет точности в некоторых случаях.