Подтвердить что ты не робот

Компиляция небезопасных Haskell

Кажется, что Haskell пытается быть безопасным языком и пытается помочь программисту от ошибок. Например, pred/succ выдает ошибку, если снаружи, и div 1 0 также бросает. Каковы эти безопасные вычисления Haskell и какие издержки они вызывают?

Можно ли отключить эту безопасность для GHC, так как они не должны быть необходимы в программе без ошибок? И может ли это обеспечить более высокую скорость работы?

Для бэкенда C появилась опция -ffast-math. Существуют ли такие параметры производительности для LLVM или LLVM?

4b9b3361

Ответ 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). Это просто делало операции с плавающей запятой быстрее за счет точности в некоторых случаях.