Стиль против производительности с использованием векторов
Вот код:
{-# LANGUAGE FlexibleContexts #-}
import Data.Int
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Generic as V
{-# NOINLINE f #-} -- Note the 'NO'
--f :: (Num r, V.Vector v r) => v r -> v r -> v r
--f :: (V.Vector v Int64) => v Int64 -> v Int64 -> v Int64
--f :: (U.Unbox r, Num r) => U.Vector r -> U.Vector r -> U.Vector r
f :: U.Vector Int64 -> U.Vector Int64 -> U.Vector Int64
f = V.zipWith (+) -- or U.zipWith, it doesn't make a difference
main = do
let iters = 100
dim = 221184
y = U.replicate dim 0 :: U.Vector Int64
let ans = iterate ((f y)) y !! iters
putStr $ (show $ U.sum ans)
Я скомпилировал с ghc 7.6.2
и -O2
, и это заняло 1,7 секунды.
Я попробовал несколько различных версий f
:
f x = U.zipWith (+) x
f x = (U.zipWith (+) x) . id
f x y = U.zipWith (+) x y
Версия 1 совпадает с оригиналом, в то время как версии 2 и 3 запускаются менее чем за 0,09 секунды (и INLINING
f
ничего не меняет).
Я также заметил, что если я сделаю f
полиморфным (с любой из трех сигнатур выше), то даже при" быстром " определении (например, 2 или 3) он замедляется down...to ровно 1,7 секунды. Это заставляет меня задуматься, возможно ли, что исходная проблема связана с (отсутствием) вывода типа, хотя я явно даю типы для векторного типа и типа элемента.
Меня также интересует сложение целых чисел по модулю q
:
newtype Zq q i = Zq {unZq :: i}
Как и при добавлении Int64
s, Если я пишу функцию с каждым указанным типом,
h :: U.Vector (Zq Q17 Int64) -> U.Vector (Zq Q17 Int64) -> U.Vector (Zq Q17 Int64)
Я получаю на порядок лучшую производительность, чем если бы я оставьте любой полиморфизм
h :: (Modulus q) => U.Vector (Zq q Int64) -> U.Vector (Zq q Int64) -> U.Vector (Zq q Int64)
Но я должен по крайней мере быть в состоянии удалить определенный фантомный тип! Он должен быть скомпилирован, так как я имею дело с newtype
.
Вот мои вопросы:
- откуда берется замедление?
- Что происходит в версиях 2 и 3
f
, которые каким-либо образом влияют на производительность? Мне кажется ошибкой, что (что равносильно) стиль кодирования может повлиять на производительность, как это. Есть ли другие примеры за пределами Вектор, где частичное применение функции или другие стилистические варианты влияют на производительность?
Почему полиморфизм замедляет меня на порядок независимо от того, где полиморфизм (то есть в векторном типе, в типе
Num
, обоих или фантомном типе)? Я знаю, что полиморфизм делает код медленнее, но это смешно. Есть ли вокруг него Хак?
Правка 1
Я подал выпуск на страницу векторной библиотеки. Я нашел ... GHC вопрос , относящийся к этой проблеме.
EDIT2
Я переписал вопрос, получив некоторое представление из ответа @kqr. Ниже приведен оригинал для справки.
--------------ОРИГИНАЛЬНЫЙ ВОПРОС--------------------
Вот код:
{-# LANGUAGE FlexibleContexts #-}
import Control.DeepSeq
import Data.Int
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Generic as V
{-# NOINLINE f #-} -- Note the 'NO'
--f :: (Num r, V.Vector v r) => v r -> v r -> v r
--f :: (V.Vector v Int64) => v Int64 -> v Int64 -> v Int64
--f :: (U.Unbox r, Num r) => U.Vector r -> U.Vector r -> U.Vector r
f :: U.Vector Int64 -> U.Vector Int64 -> U.Vector Int64
f = V.zipWith (+)
main = do
let iters = 100
dim = 221184
y = U.replicate dim 0 :: U.Vector Int64
let ans = iterate ((f y)) y !! iters
putStr $ (show $ U.sum ans)
Я скомпилировал с ghc 7.6.2
и -O2
, и это заняло 1,7 секунды.
Я попробовал несколько различных версий f
:
f x = U.zipWith (+) x
f x = (U.zipWith (+) x) . U.force
f x = (U.zipWith (+) x) . Control.DeepSeq.force)
f x = (U.zipWith (+) x) . (z -> z `seq` z)
f x = (U.zipWith (+) x) . id
f x y = U.zipWith (+) x y
Версия 1 совпадает с оригиналом, версия 2 выполняется за 0,111 секунды, а версии 3-6 выполняются менее чем за 0,09 секунды (и INLINING
f
ничего не меняет).
force
помогло, но я не уверен, откуда эта лень берется. Распакованные типы не допускаются к использованию. ленивый, да?
Я попытался написать строгую версию iterate
, думая, что сам вектор должен быть ленивым:
{-# INLINE iterate' #-}
iterate' :: (NFData a) => (a -> a) -> a -> [a]
iterate' f x = x `seq` x : iterate' f (f x)
, но с точкой-бесплатная версия f
, это не поможет.
f
полиморфным (с любой из трех сигнатур выше), даже с" быстрым " определением, он замедляется down...to ровно 1,7 секунды. Это заставляет меня задуматься, не является ли первоначальная проблема, возможно, из-за к (отсутствию) вывода типа, хотя все должно быть выведено красиво.
Вот мои вопросы:
- откуда берется замедление?
- Почему составление с помощью
force
помогает, а использование строгогоiterate
- Нет?
Почему - Почему полиморфизм замедляет меня на порядок независимо от того, где находится полиморфизм (то есть в векторном типе, в типе
Num
или в обоих)?
Почему версии 5 и 6, ни одна из которых не должна иметь никаких последствий строгости вообще, так же быстры, как строгая функция?
U.force
хуже, чем DeepSeq.force
? Я понятия не имею, что U.force
должен делать, но это звучит очень похоже на DeepSeq.force
и, кажется, имеет аналогичный эффект.
zipWith
, а не распакованную версию. Это просто случайность между GHC и Векторная библиотека, или здесь можно сказать что-то более общее?1 ответ:
Хотя у меня нет окончательного ответа, который вам нужен, есть две вещи, которые могут помочь вам.
Во-первых,x `seq` x
является, как семантически, так и вычислительно, тем же самым, что и простоx
. Вики говорит оseq
:Распространенное заблуждение относительноВ первом абзаце говорится, что написаниеseq
состоит в том, чтоseq x
"оценивает"x
. Ну, вроде того.seq
ничего не оценивает просто в силу существования в исходном файле, все, что он делает, - это вводит искусственную зависимость данных от одно значение на другом: когда результатseq
вычисляется, первый аргумент также должен быть вычислен (своего рода; см. ниже).В качестве примера предположим
x :: Integer
, тогдаseq x b
ведет себя по существу так же, какif x == 0 then b else b
– безусловно равноb
, но вынуждаяx
по пути. В частности, выражениеx `seq` x
является полностью избыточным и всегда имеет точно такой же эффект, как и просто записьx
.seq a b
не означает, чтоa
будет волшебным образом оценено в этот момент, это означает, чтоa
будет оценено, как толькоb
нужно будет оценить. Это может произойти позже в программе, или, возможно, никогда вообще. Когда вы рассматриваете его в этом свете, очевидно, чтоseq x x
является избыточностью, потому что все, что он говорит: "оценитеx
, Как толькоx
необходимо оценить."Что, конечно, произошло бы в любом случае, если бы вы просто написалиx
.Это имеет два следствия для вы:
ваша" строгая " функцияiterate'
на самом деле не строже, чем она была бы безseq
. На самом деле, мне трудно представить, как функцияiterate
может стать еще более строгой, чем она уже есть. Вы не можете сделать хвост списка строгим, потому что он бесконечен. Главное, что вы можете сделать, это заставить "аккумулятор",f x
, но это не дает никакого значительного увеличения производительности в моей системе.[1]Царапина. Ваш строгий
iterate'
делает точно то же самое, что и моя версия модели взрыва. Смотрите комментарии.Написание
(\z -> z `seq` z)
не дает вам строгой функции идентификации, которая, как я предполагаю, является тем, к чему вы стремились. На самом деле, общая функция идентификации настолько строга, насколько Вы ее получите – она оценит свой результат, как только это будет необходимо.Тем не менее, я заглянул в ядро GHC генерирует для
U.zipWith (+) y
И
U.zipWith (+) y . id
И есть только одна большая разница, что мой нетренированный глаз может заметить. Первый использует просто простой
Data.Vector.Generic.zipWith
(вот где может сыграть роль ваше совпадение полиморфизма – если GHC выберет общийzipWith
, он, конечно, будет работать, как если бы код был полиморфным!) в то время как последний разорвал этот единственный вызов функции почти на 90 строк кода монады состояния и распакованных типов машин.Код монады состояния выглядит почти как циклы и деструктивные обновления, которые вы написали бы на императивном языке, поэтому я предполагаю, что это очень хорошо подогнан к машине, на которой он работает. Если бы я не был в такой спешке, я бы взял более длинный взгляд, чтобы увидеть более точно, как это работает и почему GHC внезапно решил, что ему нужна плотная петля. Я прикрепил сгенерированное ядро столько же для себя, сколько и для любого другого, кто хочет взглянуть.[2]
[1]: форсирование аккумулятора по пути: (это то, что вы уже делаете, я неправильно понял код!)
{-# LANGUAGE BangPatterns #-} iterate' f !x = x : iterate f (f x)