Эффективная работа с битами в реализации LFSR
хотя у меня есть хорошая реализация LSFR C, я думал, что попробую то же самое в Haskell - просто посмотреть, как это происходит. То, что я придумал, до сих пор на два порядка медленнее, чем реализация C, что вызывает вопрос:как можно улучшить представление? очевидно, что операции с битами являются узким местом, и профилировщик подтверждает это.
вот базовый код Haskell с использованием списков и Data.Bits:
import Control.Monad (when)
import Data.Bits (Bits, shift, testBit, xor, (.&.), (.|.))
import System.Environment (getArgs)
import System.Exit (exitFailure, exitSuccess)
tap :: [[Int]]
tap = [
[], [], [], [3, 2],
[4, 3], [5, 3], [6, 5], [7, 6],
[8, 6, 5, 4], [9, 5], [10, 7], [11, 9],
[12, 6, 4, 1], [13, 4, 3, 1], [14, 5, 3, 1], [15, 14],
[16,15,13,4], [17, 14], [18, 11], [19, 6, 2, 1],
[20, 17], [21, 19], [22, 21], [23, 18],
[24,23,22,17], [25, 22], [26, 6, 2, 1], [27, 5, 2, 1],
[28, 25], [29, 27], [30, 6, 4, 1], [31, 28],
[32,22,2,1], [33,20], [34,27,2,1], [35,33],
[36,25], [37,5,4,3,2,1],[38,6,5,1], [39,35],
[40,38,21,19], [41,38], [42,41,20,19], [43,42,38,37],
[44,43,18,17], [45,44,42,41], [46,45,26,25], [47,42],
[48,47,21,20], [49,40], [50,49,24,23], [51,50,36,35],
[52,49], [53,52,38,37], [54,53,18,17], [55,31],
[56,55,35,34], [57,50], [58,39], [59,58,38,37],
[60,59], [61,60,46,45], [62,61,6,5], [63,62] ]
xor' :: [Bool] -> Bool
xor' = foldr xor False
mask :: (Num a, Bits a) => Int -> a
mask len = shift 1 len - 1
advance :: Int -> [Int] -> Int -> Int
advance len tap lfsr
| d0 = shifted
| otherwise = shifted .|. 1
where
shifted = shift lfsr 1 .&. mask len
d0 = xor' $ map (testBit lfsr) tap'
tap' = map (subtract 1) tap
main :: IO ()
main = do
args <- getArgs
when (null args) $ fail "Usage: lsfr <number-of-bits>"
let len = read $ head args
when (len < 8) $ fail "No need for LFSR"
let out = last $ take (shift 1 len) $ iterate (advance len (tap!!len)) 0
if out == 0 then do
putStr "OKn"
exitSuccess
else do
putStr "FAILn"
exitFailure
в основном он проверяет, является ли LSFR определена в tap :: [[Int]] на любой бит-длина максимальной длины. (Точнее, он просто проверяет, достигает ли LSFR начального состояния (нуля) после 2n итераций.)
согласно профилировщику самая дорогая линия бит обратной связи d0 = xor' $ map (testBit lfsr) tap'.
что я пробовал до сих пор:
- использовать
Data.Array: попытка оставлена, потому что нет foldl/r - использовать
Data.Vector: Немного быстрее, чем базовый
параметры компилятора, которые я использую: -O2, LTS Haskell 8.12 (GHC-8.0.2).
справочную программу на C++ можно найти на gist.github.com.
код Haskell не может быть ожидаемым (?) чтобы работать так же быстро, как код C, но два порядка величины слишком много, должен быть лучший способ сделать бит-скрипку.
Update: результаты применения оптимизаций, предложенных в ответы
- справочная программа C++ с входным сигналом 28, скомпилированная с LLVM 8.0.0, работает в 0.67 s на моей машине (то же самое с clang 3.7 немного медленнее, 0.68 s)
- базовый код Haskell работает примерно на 100 раз медленнее (из-за неэффективности пространства не пытайтесь использовать его с входами больше 25)
- с переписыванием @Thomas M. DuBuisson, все еще используя бэкэнд GHC по умолчанию, время выполнения идет вниз к 5.2 s
- с переписыванием @Thomas M. DuBuisson, теперь используя бэкэнд LLVM (опция GHC
-O2 -fllvm), время выполнения идет вниз до 1.7 с- использование опции GHC
-O2 -fllvm -optlc -mcpu=nativeдоводит это до 0.73 s
- использование опции GHC
- замена
iterateСiterate'@cirdec не имеет значения, когда используется код Томаса (как с "родным" бэкэндом по умолчанию, так и с LLVM). Однако это тут сделайте разницу, когда базовый код используемый.
Итак, мы пришли от 100x до 8x до 1.09 x, т. е. только на 9% медленнее, чем C!
Примечание
Бэкэнд LLVM для GHC 8.0.2 требует LLVM 3.7. На Mac OS X это означает установку этой версии с brew и затем symlinking opt и llc. См.7.10. GHC Backends.
3 ответов
Передние Дела
для начала я использую GHC 8.0.1 на Intel I5 ~2.5 GHz, linux x86-64.
Первый Черновик: О Нет! Время замедляется!
ваш начальный код с параметром 25 работает:
% ghc -O2 orig.hs && time ./orig 25
[1 of 1] Compiling Main ( orig.hs, orig.o )
Linking orig ...
OK
./orig 25 7.25s user 0.50s system 99% cpu 7.748 total
таким образом, время бить 77ms - на два порядка лучше, чем этот код Haskell. Давайте нырять.
Выпуск 1: Шифти Код
я нашел пару странности с кодом. Сначала было использование shift в коде высокой эффективности. Shift поддерживает как левый, так и правый сдвиг, и для этого требуется ветвь. Давайте убьем это с более читаемыми способностями двух и таких (shift 1 x~>2^x и shift x 1~>2*x):
% ghc -O2 noShift.hs && time ./noShift 25
[1 of 1] Compiling Main ( noShift.hs, noShift.o )
Linking noShift ...
OK
./noShift 25 0.64s user 0.00s system 99% cpu 0.637 total
(как вы отметили в комментариях: Да, это связано с расследованием. Возможно, какая-то странность предыдущего кода препятствовала запуску правила перезаписи и, как следствие, гораздо худшему коду результат)
Проблема 2: Списки Битов? Int операции сохранить день!
одно изменение, один порядок величины. Ура. Что еще? Ну, у вас есть этот неудобный список битовых местоположений, которые вы нажимаете, что просто кажется, что он умоляет о неэффективности и / или опирается на хрупкие оптимизации. На этом этапе я отмечу, что жесткое кодирование любого выбора из этого списка приводит к действительно хорошей производительности (например,testBit lsfr 24 `xor` testBit lsfr 21), но мы хотим более общем быстро решение.
я предлагаю вычислить маску все места крана затем сделать одну инструкцию поп-счет. Для этого нам нужен только один Int принят в advance вместо целого списка. Инструкция popcount требует хорошей генерации сборки, которая требует llvm и, вероятно,-optlc-mcpu=native или другой выбор набора инструкций, который не является пессимистичным.
этот шаг дает нам pc ниже. Я сложил в карауле-удаление advance об этом говорилось в комментариях:
let tp = sum $ map ((2^) . subtract 1) (tap !! len)
pc lfsr = fromEnum (even (popCount (lfsr .&. tp)))
mask = 2^len - 1
advance' :: Int -> Int
advance' lfsr = (2*lfsr .&. mask) .|. pc lfsr
out :: Int
out = last $ take (2^len) $ iterate advance' 0
наша результирующая производительность:
% ghc -O2 so.hs -fforce-recomp -fllvm -optlc-mcpu=native && time ./so 25
[1 of 1] Compiling Main ( so.hs, so.o )
Linking so ...
OK
./so 25 0.06s user 0.00s system 96% cpu 0.067 total
это более двух порядков величины от начала до конца, поэтому, надеюсь, он соответствует вашему C. наконец, в развернутом коде на самом деле очень часто встречаются пакеты Haskell с привязками C, но это часто учебное упражнение, поэтому я надеюсь, что вам было весело.
Edit: теперь доступный код C++ принимает мою систему 0.10 (g++ -O3) и 0.12 (clang++ -O3 -march=native) секунд, так что кажется, мы немного превзошли нашу цель.
я подозреваю, что следующая строка строит большой список, как thunk в памяти, прежде чем оценивать его.
let out = last $ take (shift 1 len) $ iterate (advance len (tap!!len)) 0` is
-rtsopts кроме -O2. Это позволяет запускать программу с помощью параметры RTS, inclusing +RTS -s который выводит небольшую память резюме.
Начальная Производительность
запуск вашей программы как lfsr 25 +RTS -s я получаю следующий вывод
OK
5,420,148,768 bytes allocated in the heap
6,705,977,216 bytes copied during GC
1,567,511,384 bytes maximum residency (20 sample(s))
357,862,432 bytes maximum slop
3025 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 10343 colls, 0 par 2.453s 2.522s 0.0002s 0.0009s
Gen 1 20 colls, 0 par 2.281s 3.065s 0.1533s 0.7128s
INIT time 0.000s ( 0.000s elapsed)
MUT time 1.438s ( 1.162s elapsed)
GC time 4.734s ( 5.587s elapsed)
EXIT time 0.016s ( 0.218s elapsed)
Total time 6.188s ( 6.967s elapsed)
%GC time 76.5% (80.2% elapsed)
Alloc rate 3,770,538,273 bytes per MUT second
Productivity 23.5% of total user, 19.8% of total elapsed
это много памяти, используемой сразу. Очень вероятно, что где-то там есть большое здание.
пытается уменьшить размер thunk
я предположил, что thunk строится в iterate (advance ...). Если это так, мы можем попытаться уменьшить размер thunk, сделав advance более строгий в своем lsfr аргумент. Это не удалит позвоночник thunk (последовательные итерации), но это может уменьшить размер состояния, которое создается по мере оценки позвоночника.
BangPatterns - простой способ сделать функцию строгой в аргументе. f !x = .. сокращенно от f x = seq x $ ...
{-# LANGUAGE BangPatterns #-}
advance :: Int -> [Int] -> Int -> Int
advance len tap = go
where
go !lfsr
| d0 = shifted
| otherwise = shifted .|. 1
where
shifted = shift lfsr 1 .&. mask len
d0 = xor' $ map (testBit lfsr) tap'
tap' = map (subtract 1) tap
давайте посмотрим, что это меняет ...
>lfsr 25 +RTS -s
OK
5,420,149,072 bytes allocated in the heap
6,705,979,368 bytes copied during GC
1,567,511,448 bytes maximum residency (20 sample(s))
357,862,448 bytes maximum slop
3025 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 10343 colls, 0 par 2.688s 2.711s 0.0003s 0.0059s
Gen 1 20 colls, 0 par 2.438s 3.252s 0.1626s 0.8013s
INIT time 0.000s ( 0.000s elapsed)
MUT time 1.328s ( 1.146s elapsed)
GC time 5.125s ( 5.963s elapsed)
EXIT time 0.000s ( 0.226s elapsed)
Total time 6.484s ( 7.335s elapsed)
%GC time 79.0% (81.3% elapsed)
Alloc rate 4,081,053,418 bytes per MUT second
Productivity 21.0% of total user, 18.7% of total elapsed
ничего заметного.
исключения позвоночника
я думаю, это позвоночник это iterate (advance ...) это строится. В конце концов, для команды, которую я запускаю, список будет 2^25, или чуть более 33 миллионов пунктов. Сам список, вероятно, удаляется список fusion, но thunk для последнего элемента списка составляет более 33 миллионов приложений advance ...
чтобы решить эту проблему, нам нужна строгая версия iterate так что значение принудительно к Int перед нанесением advance функции снова. Это должно держать память до одного lfsr значение в то время, наряду с в настоящее время вычисляется применение advance.
к сожалению, нет строгой iterate на Data.List. Вот один, который не отказывается от списка fusion, который предоставляет другие важные (Я думаю) оптимизации производительности для этой проблемы.
{-# LANGUAGE BangPatterns #-}
import GHC.Base (build)
{-# NOINLINE [1] iterate' #-}
iterate' :: (a -> a) -> a -> [a]
iterate' f = go
where go !x = x : go (f x)
{-# NOINLINE [0] iterateFB' #-}
iterateFB' :: (a -> b -> b) -> (a -> a) -> a -> b
iterateFB' c f = go
where go !x = x `c` go (f x)
{-# RULES
"iterate'" [~1] forall f x. iterate' f x = build (\c _n -> iterateFB' c f x)
"iterateFB'" [1] iterateFB' (:) = iterate'
#-}
это просто iterate С GHC.List (вместе со всеми своими правилами переписывания), но сделанный строгим в аккумулированном аргумент.
оснащен строгой итерации,iterate', мы можем изменить хлопотно строки
let out = last $ take (shift 1 len) $ iterate' (advance len (tap!!len)) 0
я ожидаю, что это будет работать гораздо лучше. Давайте посмотрим ...
>lfsr 25 +RTS -s
OK
3,758,156,184 bytes allocated in the heap
297,976 bytes copied during GC
43,800 bytes maximum residency (1 sample(s))
21,736 bytes maximum slop
1 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 7281 colls, 0 par 0.047s 0.008s 0.0000s 0.0000s
Gen 1 1 colls, 0 par 0.000s 0.000s 0.0002s 0.0002s
INIT time 0.000s ( 0.000s elapsed)
MUT time 0.750s ( 0.783s elapsed)
GC time 0.047s ( 0.008s elapsed)
EXIT time 0.000s ( 0.000s elapsed)
Total time 0.797s ( 0.792s elapsed)
%GC time 5.9% (1.0% elapsed)
Alloc rate 5,010,874,912 bytes per MUT second
Productivity 94.1% of total user, 99.0% of total elapsed
это 0.00002 раз больше памяти и работает в 10 раз быстрее.
я не знаю, если это улучшит на ответ, что улучшает advance но все равно оставляет ленивый iterate advance' на месте. Было бы легко проверить; добавьте iterate' код к этому ответу и используйте iterate' на месте iterate в ответ.
-
поднимает ли компилятор
tap !! lenвыход из цикла? Я подозреваю, что это так, но перемещение его, чтобы гарантировать, что это не повредит:let tap1 = tap !! len let out = last $ take (shift 1 len) $ iterate (advance len tap1) 0 -
в комментариях вы говорите:"
2^lenнужен ровно один раз", но это неправильно. Вы делаете это каждый раз вadvance. Так что вы можете попробоватьadvance len tap mask lfsr | d0 = shifted | otherwise = shifted .|. 1 where shifted = shift lfsr 1 .&. mask d0 = xor' $ map (testBit lfsr) tap' tap' = map (subtract 1) tap -- in main let tap1 = tap !! len let numIterations = 2^len let mask = numIterations - 1 let out = iterate (advance len tap1 mask) 0 !! (numIterations - 1)(компилятор не может оптимизировать
last $ take ...to!!в целом, потому что они разные для конечных списков, ноiterateвсегда возвращает бесконечный один.) вы сравнили
foldrСfoldl, аfoldlпочти никогда не то, что вам нужно; Сxorвсегда нуждается в обоих Аргументах и является ассоциативным,foldl', скорее всего, будет правильным выбором (компилятор может оптимизировать его, но если есть какая-либо реальная разница междуfoldlиfoldrи не просто случайная вариация, в этом случае она могла бы потерпеть неудачу).