Плохая производительность с транспонированием и накопленной суммой в Repa - PullRequest
19 голосов
/ 10 июня 2011

Я разработал функцию кумулятивной суммы, как определено ниже в библиотеке Haskell Repa.Однако я столкнулся с проблемой при объединении этой функции с операцией транспонирования.Все 3 из следующих операций занимают меньше секунды:

cumsum $ cumsum $ cumsum x
transpose $ transpose $ transpose x
transpose $ cumsum x

Однако, если я напишу:

cumsum $ transpose x

производительность ужасно ухудшается.В то время как каждая отдельная операция занимает несколько секунд на изображении 1920x1080, при объединении они теперь занимают более 30 секунд ...

Есть идеи, что может быть причиной этого?Моя интуиция говорит мне, что это как-то связано с отложенными массивами, не форсированием в нужное время и т. Д ... Но у меня пока недостаточно опыта, чтобы отследить это.

1 Ответ

25 голосов
/ 14 июня 2011

С точки зрения разработчика библиотеки, способ отладки состоит в том, чтобы создать оболочку для подозрительной операции, а затем посмотреть на основной код, чтобы убедиться, что слияние сработало.

-- Main.hs ---------------------------------------------------
import Solver
import Data.Array.Repa.IO.BMP

main 
 = do   Right img       <- readImageFromBMP "whatever.bmp"
        print $ cumsumBMP img

-- Solver.hs --------------------------------------------------
{-# LANGUAGE TypeOperators, FlexibleContexts, TypeFamilies #-}
module Solver (cumsumBMP) where
import Data.Array.Repa  as Repa
import Data.Word

{- all your defs -}

{-# NOINLINE cumsumBMP #-}
cumsumBMP :: Array DIM3 Word8 -> Array DIM3 Word8
cumsumBMP img = cumsum $ transpose img

Я поставил«решающий» код в отдельном модуле, поэтому нам нужно только пробежаться по коду ядра для определений, которые нас интересуют.

Компилировать как:

touch Solver.hs ; ghc -O2 --make Main.hs \
 -ddump-simpl -dsuppress-module-prefixes -dsuppress-coercions  > dump

Перейти к определениюcumsumBMP и найдите ключевое слово letrec.Поиск letrec - это быстрый способ найти внутренние петли.

Не слишком далеко, я вижу это: (немного переформатирован)

case gen_a1tr
of _ {
  GenManifest vec_a1tv ->
    case sh2_a1tc  `cast` ... of _ { :. sh3_a1iu  sh4_a1iv ->
    case ix'_a1t9  `cast` ... of _ { :. sh1'_a1iz sh2'_a1iA ->
    case sh3_a1iu  `cast` ... of _ { :. sh5_X1n0  sh6_X1n2 ->
    case sh1'_a1iz `cast` ... of _ { :. sh1'1_X1n9 sh2'1_X1nb ->
    case sh5_X1n0             of _ { :. sh7_X1n8   sh8_X1na ->
    ...
    case sh2'1_X1nb           of _ { I# y3_X1nO ->
    case sh4_a1iv             of _ { I# y4_X1nP ->
    case sh2'_a1iA            of _ { I# y5_X1nX ->
    ...
    let { x3_a1x6 :: Int# [LclId]
      x3_a1x6 =
        +#
          (*#
             (+#
                (*#
                   y1_a1iM
                   y2_X1nG)
                y3_X1nO)
             y4_X1nP)
          y5_X1nX } in
    case >=#
           x3_a1x6
           0
    of ...

Бедствие!Привязка x3_a1x6 явно выполняет некоторую полезную работу (умножение, сложение и тому подобное), но она заключена в длинный ряд операций распаковки, которые также выполняются для каждой итерации цикла.Хуже всего то, что он распаковывает длину и ширину (форму) массива на каждой итерации, и эта информация всегда будет одинаковой.GHC действительно должен вывести эти выражения из цикла, но это пока не так.Это экземпляр Проблема # 4081 в GHC trac , которая, надеюсь, будет исправлена ​​в ближайшее время.

Обходной путь - применить deepSeqArray к входящему массиву.Это накладывает спрос на его значение на верхнем уровне (вне цикла), что позволяет GHC знать, что можно продолжать перемещение совпадений.Для такой функции, как cumsumBMP, мы также ожидаем, что входящий массив уже будет манифестом, поэтому мы можем добавить явное совпадение регистра для этого:

{-# NOINLINE cumsumBMP #-}
cumsumBMP :: Array DIM3 Word8 -> Array DIM3 Word8
cumsumBMP img@(Array _ [Region RangeAll (GenManifest _)])
  = img `deepSeqArray` cumsum $ transpose img

Снова компилируя, внутренний цикл теперь выглядит намного лучше:

letrec {
$s$wfoldlM'_loop_s2mW [...]
  :: Int# -> Word# -> Word# [...]
$s$wfoldlM'_loop_s2mW =
  \ (sc_s2mA :: Int#) (sc1_s2mB :: Word#) ->
    case <=# sc_s2mA a_s2ji of _ {
      False -> sc1_s2mB;
      True ->
        $s$wfoldlM'_loop_s2mW
          (+# sc_s2mA 1)
          (narrow8Word#
             (plusWord#
                sc1_s2mB
                (indexWord8Array#
                   rb3_a2gZ
                   (+#
                      rb1_a2gX
                      (+#
                         (*#
                            (+#
                               (*#
                                  wild19_X1zO
                                  ipv1_X1m5)
                               sc_s2mA)
                            ipv2_X1m0)
                         wild20_X1Ct)))))
    }; } in

Это узкий хвостовой рекурсивный цикл, который использует только примитивные операции.При условии, что вы скомпилируете с -fllvm -optlo-O3, нет причины, по которой он не будет работать так же быстро, как эквивалентная программа на Си.

При запуске программы возможен небольшой сбой:

desire:tmp benl$ ./Main 
Main: Solver.hs:(50,1)-(51,45): Non-exhaustive patterns in function cumsumBMP

Это простонапоминает нам о том, что перед вызовом cumsumBMP.

-- Main.hs ---------------------------------------------------
...
import Data.Array.Repa as Repa
main 
 = do   Right img       <- readImageFromBMP "whatever.bmp"
        print $ cumsumBMP $ Repa.force img

нам нужно форсировать массив. В итоге:

  1. Вам необходимо добавить в свой цикл deepSeqArray и шаблон соответствияфункции верхнего уровня для обхода текущей погрешности в GHC.Это продемонстрировано в окончательной версии функции cumsumBMP выше.Если вы хотите, чтобы GHC HQ исправил это в ближайшее время, добавьте себя как cc к Issue # 4081 на GHC trac .Программы исправления будут намного красивее, когда это будет исправлено.
  2. Вам не нужно добавлять цикл к каждой функции.В этом примере мне не нужно было трогать indexSlice и друзей.Общее правило заключается в добавлении группы к функциям, которые используют force, fold или sumAll.Эти функции создают действительные циклы, которые работают с данными массива, то есть они преобразуют задержанный массив в значение манифеста.
  3. Производительность фрагмента кода Repa в той же степени определяется контекстом, в котором ониспользуется в качестве фактического кода.Если вы передадите своим задержанным массивам функции верхнего уровня, они будут работать очень медленно.Более подробно об этом говорится в Учебное пособие по Repa .
  4. BMP-файлы, считываемые из библиотеки repa-io, предварительно не форсируются, поэтому перед их использованием необходимо принудительно их принудительно применить.Возможно, это неправильное значение по умолчанию, поэтому я изменю его в следующей версии.
...