Эффективная работа с битами в реализации 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
и не просто случайная вариация, в этом случае она могла бы потерпеть неудачу).