Эффективность unfoldr против zipWith
на проверку кода, я ответил на вопрос о наивное решение Haskell fizzbuzz предложив реализацию, что проходит вперед, избегая квадратичной стоимости увеличения числа простых чисел и отбрасывая деление по модулю (почти) полностью. Вот код:
fizz :: Int -> String
fizz = const "fizz"
buzz :: Int -> String
buzz = const "buzz"
fizzbuzz :: Int -> String
fizzbuzz = const "fizzbuzz"
fizzbuzzFuncs = cycle [show, show, fizz, show, buzz, fizz, show, show, fizz, buzz, show, fizz, show, show, fizzbuzz]
toFizzBuzz :: Int -> Int -> [String]
toFizzBuzz start count =
let offsetFuncs = drop (mod (start - 1) 15) fizzbuzzFuncs
in take count $ zipWith ($) offsetFuncs [start..]
в качестве дополнительного приглашения я предложил переписать его с помощью Data.List.unfoldr
. The unfoldr
версия является очевидной, простой модификацией этого кода, поэтому я не собираюсь введите его здесь, если люди, желающие ответить на мой вопрос, настаивают, что это важно (нет спойлеров для OP над обзором кода). Но у меня есть вопрос об относительной эффективности unfoldr
решение по сравнению с zipWith
один. Хотя я больше не Неофит Хаскелла, я не эксперт по внутренностям Хаскелла.
An unfoldr
решение не требует [start..]
бесконечный список, так как он может просто развернутся от start
. Мои мысли
- в
zipWith
решение не запоминает каждый последующий элемент[start..]
как его просят. Каждый элемент используется и отбрасывается, потому что нет ссылки на начало [start..] содержащийся. Таким образом, там потребляется не больше памяти, чем сunfoldr
. - опасения по поводу производительности
unfoldr
и последние патчи, чтобы сделать его всегда встроенным, проводятся на уровне, которого я еще не достиг.
поэтому я думаю, что эти два эквивалентны в потреблении памяти, но понятия не имею об относительной производительности. Надеясь, что больше прошлого года Haskellers может направить меня к пониманию этого.
unfoldr
Кажется естественным использовать для генерации последовательностей, даже если другие решения более выразительны. Я просто знаю, что мне нужно больше понять о реальной производительности. (Почему-то я нахожу foldr
гораздо легче понять на этом уровне)
Примечание: unfoldr
С использованием Maybe
была первая потенциальная производительность проблема, которая пришла мне в голову, прежде чем я даже начал исследовать проблему (и единственный бит оптимизационных/встроенных обсуждений, которые я полностью понял). Таким образом, я смог перестать беспокоиться о Maybe
сразу (учитывая последнюю версию Haskell).
1 ответов
как ответственный за последние изменения в реализациях zipWith
и unfoldr
, я подумал, что мне, вероятно,стоит попробовать. Я не могу сравнивать их так легко, потому что они очень разные функции, но я могу попытаться объяснить некоторые из их свойств и значение изменений.
unfoldr
Inlining
старая версия unfoldr
(ранее base-4.8
/ GHC 7.10) был рекурсивным на верхнем уровне (он назывался сама напрямую). GHC никогда не строит рекурсивные функции, поэтому unfoldr
никогда не была подставлена. В результате GHC не мог видеть, как он взаимодействовал с переданной функцией. Самым тревожным следствием этого было то, что функция передавалась, типа (b -> Maybe (a, b))
, фактически произвести Maybe (a, b)
значения, выделяя память для удержания Just
и (,)
конструкторы. Путем реструктуризации unfoldr
как "работник" и "оболочка", новый код позволяет GHC встроить его и (во многих случаях) сплавить его с функция передана, поэтому дополнительные конструкторы удаляются оптимизациями компилятора.
например, под GHC 7.10, код
module Blob where
import Data.List
bloob :: Int -> [Int]
bloob k = unfoldr go 0 where
go n | n == k = Nothing
| otherwise = Just (n * 2, n+1)
составлен с ghc -O2 -ddump-simpl -dsuppress-all -dno-suppress-type-signatures
ведет к ядру
$wbloob :: Int# -> [Int]
$wbloob =
\ (ww_sYv :: Int#) ->
letrec {
$wgo_sYr :: Int# -> [Int]
$wgo_sYr =
\ (ww1_sYp :: Int#) ->
case tagToEnum# (==# ww1_sYp ww_sYv) of _ {
False -> : (I# (*# ww1_sYp 2)) ($wgo_sYr (+# ww1_sYp 1));
True -> []
}; } in
$wgo_sYr 0
bloob :: Int -> [Int]
bloob =
\ (w_sYs :: Int) ->
case w_sYs of _ { I# ww1_sYv -> $wbloob ww1_sYv }
Fusion
другое изменение unfoldr
переписывал его для участия в fold / build fusion, структуре оптимизации, используемой в библиотеках списков GHC. Идея как" сложить/построить "слияние, так и более новый, по-разному сбалансированный" поток fusion " (используется в vector
библиотека) заключается в том, что если список производится "хорошим производителем", преобразуется "хорошими трансформаторами" и потребляется "хорошим потребителем", то список никогда не нужно выделять вообще. Старый unfoldr
был не хороший производитель, поэтому, если вы создали список с unfoldr
и потребил его с, скажем, foldr
, части списка будут выделены (и сразу же станут мусором) по мере продолжения вычислений. Теперь,unfoldr
- это хорошо продюсер, так что вы можете написать цикл, используя, скажем,unfoldr
, filter
и foldr
, и не (обязательно) выделять какую-либо память вообще.
например, учитывая приведенное выше определение bloob
, и суровый {-# INLINE bloob #-}
(этот материал немного хрупкий; хорошие производители иногда должны быть явно встроены, чтобы быть хорошими), код
hooby :: Int -> Int
hooby = sum . bloob
компилируется в GHC core
$whooby :: Int# -> Int#
$whooby =
\ (ww_s1oP :: Int#) ->
letrec {
$wgo_s1oL :: Int# -> Int# -> Int#
$wgo_s1oL =
\ (ww1_s1oC :: Int#) (ww2_s1oG :: Int#) ->
case tagToEnum# (==# ww1_s1oC ww_s1oP) of _ {
False -> $wgo_s1oL (+# ww1_s1oC 1) (+# ww2_s1oG (*# ww1_s1oC 2));
True -> ww2_s1oG
}; } in
$wgo_s1oL 0 0
hooby :: Int -> Int
hooby =
\ (w_s1oM :: Int) ->
case w_s1oM of _ { I# ww1_s1oP ->
case $whooby ww1_s1oP of ww2_s1oT { __DEFAULT -> I# ww2_s1oT }
}
который не имеет списков, нет Maybe
s и без пар; единственное распределение, которое он выполняет это Int
используется для хранения конечного результата (приложение I#
to ww2_s1oT
). Весь расчет может быть выполнено в регистрах машины.
zipWith
zipWith
имеет немного странную историю. Он вписывается в структуру fold/build немного неуклюже (я считаю, что он работает немного лучше с потоковым слиянием). Можно сделать zipWith
fuse с его первым или вторым аргументом списка, и в течение многих лет список библиотека попыталась сделать так, чтобы он слился с любым, если любой из них был хорошим продюсером. К сожалению, сделать его плавким со своим вторым аргументом списка может сделать программу менее определенными при определенных обстоятельствах. То есть программа, использующая zipWith
может работать нормально при компиляции без оптимизации, но выдаст ошибку при компиляции с оптимизацией. Это не очень хорошая ситуация. Следовательно, по состоянию на base-4.8
, zipWith
больше не пытается слиться со своим вторым аргументом списка. Если хочешь. чтобы слиться с хорошим продюсером, этот хороший продюсер должен быть в первом списке аргументов.
в частности, примером реализации zipWith
приводит к ожиданию, что, скажем,zipWith (+) [1,2,3] (1 : 2 : 3 : undefined)
даст [2,4,6]
, потому что он останавливается, как только он попадает в конец первого списка. С предыдущим zipWith
реализация, если второй список выглядел так, но был произведен хорошим производителем, и если zipWith
случилось слиться с ним, а не с первым списком, тогда он пойдет бум.