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


Вот код:

{-# 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:

  1. f x = U.zipWith (+) x
  2. f x = (U.zipWith (+) x) . id
  3. 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}

Как и при добавлении Int64s, Если я пишу функцию с каждым указанным типом,

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.

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

  1. откуда берется замедление?
  2. Что происходит в версиях 2 и 3 f, которые каким-либо образом влияют на производительность? Мне кажется ошибкой, что (что равносильно) стиль кодирования может повлиять на производительность, как это. Есть ли другие примеры за пределами Вектор, где частичное применение функции или другие стилистические варианты влияют на производительность?
  3. Почему полиморфизм замедляет меня на порядок независимо от того, где полиморфизм (то есть в векторном типе, в типе 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:

  1. f x = U.zipWith (+) x
  2. f x = (U.zipWith (+) x) . U.force
  3. f x = (U.zipWith (+) x) . Control.DeepSeq.force)
  4. f x = (U.zipWith (+) x) . (z -> z `seq` z)
  5. f x = (U.zipWith (+) x) . id
  6. 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 секунды. Это заставляет меня задуматься, не является ли первоначальная проблема, возможно, из-за к (отсутствию) вывода типа, хотя все должно быть выведено красиво.

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

  1. откуда берется замедление?
  2. Почему составление с помощью force помогает, а использование строгого iterate - Нет?
  3. Почему U.force хуже, чем DeepSeq.force? Я понятия не имею, что U.force должен делать, но это звучит очень похоже на DeepSeq.force и, кажется, имеет аналогичный эффект.
  4. Почему полиморфизм замедляет меня на порядок независимо от того, где находится полиморфизм (то есть в векторном типе, в типе Num или в обоих)?
  5. Почему версии 5 и 6, ни одна из которых не должна иметь никаких последствий строгости вообще, так же быстры, как строгая функция?
Как отметил @kqr, проблема, похоже, не в строгости. Таким образом, что-то в том, как я пишу функцию, заставляет использовать generic zipWith, а не распакованную версию. Это просто случайность между GHC и Векторная библиотека, или здесь можно сказать что-то более общее?
1 14

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.

Это имеет два следствия для вы:

  1. ваша" строгая " функция iterate' на самом деле не строже, чем она была бы без seq. На самом деле, мне трудно представить, как функция iterate может стать еще более строгой, чем она уже есть. Вы не можете сделать хвост списка строгим, потому что он бесконечен. Главное, что вы можете сделать, это заставить "аккумулятор", f x, но это не дает никакого значительного увеличения производительности в моей системе.[1]

    Царапина. Ваш строгий iterate' делает точно то же самое, что и моя версия модели взрыва. Смотрите комментарии.

  2. Написание (\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 переводится.