Стиль и производительность с использованием векторов

Здесь код:

{-# 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), он замедляется обратно... ровно на 1,7 секунды. Это заставляет меня задаться вопросом, возможно ли исходная проблема из-за (отсутствия) вывода типа, хотя я явно даю типы типа Vector и типа элемента.

Мне также интересно добавлять целые числа по модулю 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)

Но я должен хотя бы удалить конкретный тип phantom! Он должен быть скомпилирован, так как я имею дело с newtype.

Вот мои вопросы:

  • Где происходит замедление?
  • Что происходит в версиях 2 и 3 из f, которые влияют на производительность каким-либо образом? Мне кажется, что ошибка (то, что соответствует) стиль кодирования может повлиять на производительность, как это. Существуют ли другие примеры за пределами Vector, где частично применение функции или других стилистических решений влияет на производительность?
  • Почему полиморфизм замедляет меня на порядок, независимо от того, где полиморфизм (т.е. в векторном типе, в типе Num, как, так и phantom)? Я знаю, что полиморфизм делает код медленнее, но это смешно. Есть ли взломать его?

ИЗМЕНИТЬ 1

Я подал issue на странице библиотеки Vector. Я нашел 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 помог, но я не уверен, откуда лень. Unboxed типы не могут быть ленивыми, не так ли?

Я попробовал написать строгую версию iterate, считая, что сам вектор должен быть ленивым:

{-# INLINE iterate' #-}
iterate' :: (NFData a) => (a -> a) -> a -> [a]
iterate' f x =  x `seq` x : iterate' f (f x)

но с точечной версией f это не помогло.

Я также заметил что-то еще, что может быть просто совпадением и красной селедкой: Если я сделаю f полиморфным (с любой из трех подписей выше), даже с "быстрым" определением, он замедляется обратно... ровно на 1,7 секунды. Это заставляет меня задаться вопросом, может ли исходная проблема возникнуть из-за (отсутствия) вывода типа, хотя все должно быть понятно.

Вот мои вопросы:

  • Где происходит замедление?
  • Почему создание с помощью force справки, но с использованием строгой iterate нет?
  • Почему U.force хуже, чем DeepSeq.force? Я понятия не имею, что делать U.force, но это очень похоже на DeepSeq.force и, похоже, имеет аналогичный эффект.
  • Почему полиморфизм замедляет меня на порядок, независимо от того, где полиморфизм (т.е. в векторном типе, в типе Num или в обоих)?
  • Почему версии 5 и 6, ни одна из которых не должна иметь каких-либо последствий для строгости, так же быстро, как и строгая функция?

Как указывал @kqr, проблема не выглядит строгостью. Итак, что-то о том, как я пишу эту функцию, приводит к тому, что общий zipWith используется, а не версия Unboxed. Это просто случайность между GHC и библиотекой Vector, или есть что-то более общее, что можно здесь сказать?

Ответ 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)

[2]: Какое ядро ​​ U.zipWith (+) y . id переведено в.