haskell n00b question
Jan. 6th, 2015 12:30 amДля одной задачки всхотелось мне сделать интерпретатор крайне простого язычка: есть память на дюжину целых чисел и AST из трех операций - сравнение двух ячеек, копирование из одной в другую и обмен значениями меж двух ячеек. Стал делать на хаскеле и сразу столкнулся с тем, что наивное решение в лоб тормозит: аналогичный код на окамле в 4 раза быстрее, а на Ди - в 10 раз. Вот так выглядит мой позор на хаскеле:
А вот прямой перевод на Окамл, почти вчетверо быстрее:
А теперь вопрос: как хаскельный вариант следует переписать, чтобы он стал побыстрее и выглядел не слишком страшно?
Полные тексты тестов:
Haskell - 7.19 s
OCaml - 1.85 s
D - 0.69 s (использовался DMD - наименее оптимизирующий из компиляторов D, т.е. можно еще быстрее)
Upd: исправлен баг в окамловской версии, время перемеряно.
Upd2: не, не было там бага.
data Exp = IfGt Int Int Block Block | Swap Int Int | Copy Int Int deriving Show type Block = [Exp] eval :: STUArray s Int Int -> Exp -> ST s (STUArray s Int Int, Int) eval a (IfGt i j b1 b2) = do ai <- readArray a i aj <- readArray a j let b = if ai > aj then b1 else b2 (r, n) <- evalBlock a b return (r, n+1) eval a (Swap i j) = do ai <- readArray a i aj <- readArray a j writeArray a i aj writeArray a j ai return (a, 1) eval a (Copy i j) = do aj <- readArray a j writeArray a i aj return (a, 1) evalBlock :: STUArray s Int Int -> Block -> ST s (STUArray s Int Int, Int) evalBlock a blk = foldM f (a,0) blk where f (a,cnt) exp = fmap (\(r, n) -> (r, cnt + n)) $ eval a exp
А вот прямой перевод на Окамл, почти вчетверо быстрее:
type exp = IfGt of int * int * block * block | Swap of int * int | Copy of int * int and block = exp list;; let rec eval a = function | IfGt(i, j, b1, b2) -> let (r,n) = evalBlock a (if a.(i) > a.(j) then b1 else b2) in (r, n+1) | Swap(i, j) -> let ai = a.(i) and aj = a.(j) in a.(i) <- aj; a.(j) <- ai; (a, 1) | Copy(i, j) -> a.(i) <- a.(j); (a, 1) and evalBlock a blk = let f (m, cnt) exp = let (r,n) = eval m exp in (r, cnt + n) in List.fold_left f (a,0) blk
А теперь вопрос: как хаскельный вариант следует переписать, чтобы он стал побыстрее и выглядел не слишком страшно?
Полные тексты тестов:
Haskell - 7.19 s
OCaml - 1.85 s
D - 0.69 s (использовался DMD - наименее оптимизирующий из компиляторов D, т.е. можно еще быстрее)
Upd: исправлен баг в окамловской версии, время перемеряно.
Upd2: не, не было там бага.
no subject
Date: 2015-01-05 05:50 pm (UTC)Наверно, там ленивость сыграла злую шутку, и хаскелл вместо вычислений рожает мега-санки, которые отрабатывают лишь в самом конце. А это удар по памяти, вот и тормоза.
Самле простое - натыкать форсирования вычислений гле только можно (на возврате из eval?) - оператор $! емнип.
no subject
Date: 2015-01-05 06:04 pm (UTC)Если что, STUArray - мутабельный строгий анбоксед массив, живущий в ST.
Но где-то санки могут действительно копиться, ибо аллоцируют приведенные функции как черти.
no subject
Date: 2015-01-05 06:14 pm (UTC)no subject
Date: 2015-01-05 06:15 pm (UTC)GHC 7.6.3
no subject
Date: 2015-01-05 06:19 pm (UTC)upd. Конкретно, посмотрите вот эту прагму: https://downloads.haskell.org/~ghc/7.0.2/docs/html/users_guide/pragmas.html#unpack-pragma . Потому что у вас в тесте, насколько я понимаю, в АСТ инты завернуты в отдельные танки. Нужно их как миниум распаковать, иначе сравнение нечестное.
no subject
Date: 2015-01-05 06:43 pm (UTC)data Exp = IfGt {-# UNPACK #-} !Int {-# UNPACK #-} !Int Block Block | Swap {-# UNPACK #-} !Int {-# UNPACK #-} !Int | Copy {-# UNPACK #-} !Int {-# UNPACK #-} !Int7.08 s. Крайне незначительное улучшение.
Еще пробовал unsafeRead/unsafeWrite для массива, еще около секунды дало выигрыш, тоже маловато.
no subject
Date: 2015-01-05 07:28 pm (UTC)А вообще надо смотреть core.
no subject
Date: 2015-01-05 08:15 pm (UTC)no subject
Date: 2015-01-05 08:20 pm (UTC)Если использовать Data.Foldable то начинает выполняться нормально у меня за 0.05с, при том, что изначальная версия за 5с.
Ссылку на полный код можно найти на форке гиста, т.к. боюсь в жж её не запостить.
UPD. т.е. import Data.Foldable (foldlM) и использовать его вместо foldM.
так же по желанию добавить ! к аккумулятору f, т.е. f (!a, !cnt) ...
UPD2. с Data.Foldable можно оставлять вариант с fmap и убирать там явное вычисление, т.к. ghc уже справляется.
no subject
Date: 2015-01-05 08:22 pm (UTC)no subject
Date: 2015-01-05 08:29 pm (UTC)no subject
Date: 2015-01-05 08:33 pm (UTC)К сожалению windows под рукой у меня нету, так что проверить не могу, но ситуация там может быть чуть хуже, чем на linux.
no subject
Date: 2015-01-05 08:51 pm (UTC)Я попробую еще сделать код поаккуратнее, но думаю, что вопрос в треде он снимать должен.
no subject
Date: 2015-01-05 09:56 pm (UTC)а в продакшне, матерясь, переписываешь в кашу из бангов и unsafePerformIO на MVar
no subject
Date: 2015-01-05 10:13 pm (UTC)no subject
Date: 2015-01-05 10:22 pm (UTC)В очередной версии гиста положено.
no subject
Date: 2015-01-05 11:33 pm (UTC)...И вдвое короче.
no subject
Date: 2015-01-06 12:57 am (UTC)+ import Data.Foldable as F hiding (minimum)
- evalBlock a blk = foldM f (a,0) blk where
+ evalBlock a blk = F.foldlM f (a,0) blk where
- calcScore blk = sum $ map (numSteps blk) inputs
+ calcScore blk = F.sum $ map (numSteps blk) inputs
дает разницу в производительности почти в 100 раз (ок, ладно, не в 100, у меня на ноуте в 85).
Не подскажете что читать/куда смотреть/на что обращать внимание на тему понимания производительности вот таких вот штук в х-ле?
Я вот интереса ради пытался профайлить этот пример - непонятно было вообще ничего (разве что кол-во аллокаций памяти смущало - у меня получалось что-то около 14G - но что с этим делать не понял). С одной стороны круто что от х-ля можно получить перфоманс сравнимый с D, с другой стороны выглядит как совершенно непонятный black magic.
no subject
Date: 2015-01-06 04:28 am (UTC)У меня работает за 0.075 с, т.е. в 95 раз быстрее, чем было, и почти в 10 раз быстрее D. Подозрительно быстро (4 такта на 1 узел AST?), но если это не глюк, то просто чудо.
Upd. нашел обман:
main = print $ minimum $ replicate 40 (calcScore sornet)
тут мы не 40 раз считаем, а лишь один раз. Отсюда и невероятное ускорение.
Если считать на 40 формально отличающихся программах одинакового размера
main = print . minimum . map calcScore $ [Copy 10 (x `mod` 10) : sornet | x <- [1..40]]
то получается 1.40 с, уже реалистичное время. Быстрее окамла, но вдвое медленнее D.
no subject
Date: 2015-01-06 04:54 am (UTC)Реальное ускорение вышло в 5 раз. Результат несколько быстрее окамла и вдвое медленнее Ди. См. коммент рядом.
no subject
Date: 2015-01-06 05:04 am (UTC)Замена sum на F.sum тут, скорее всего, роли не играет - насколько я понимаю, этот sum в масштабах этой программы вызывается редко и на маленьких списках.
no subject
Date: 2015-01-06 06:40 am (UTC)В принципе мне кажется там можно ещё попробовать поускорять, но это будет чёрная магия.
no subject
Date: 2015-01-06 06:41 am (UTC)no subject
Date: 2015-01-06 07:12 am (UTC)Обычно первые кандидаты на просмотр это функции с аккумулятором, как в данном случае верно заметил [user]antilamer[/user]. В таких функциях хорошо бы чтобы в документации было написано про Свёрток бывает 2:
1. Правая свертка (foldlr) (1+(1+..)), такая свертка удобна тогда когда функция ленивая по второму аргументу
2. Левая свёртка (foldl) ((1+..)+1) эта свертка удобна для строгих функций.
Если эти правила не выполняются, то возможна излишняя ленивость и потребление памяти. В данном случае мы имеем дело с левой сверткой без форсирования вычисления аккумулятора (аналог foldl), такая функция будет гарантировано слишком ленивой и для неёнеё (почти) никогда нету мест применения (см. блог у well typed).
Соответственно решение заключается в том, чтобы сделать аккумулятор строгим, напр. переписав foldM с явным `seq` аккумулятора, но или использованием готового библиотечного.
Однако проблему это не устранит, т.к. мы возвращаем кортеж, а как известно поля кортежа ленивые и форсирование значений идёт до WHNF ((,) undefined undefined) то просто строгости не хватит и надо чтобы ещё и поля вычислялиствычислялись, что и сделано с помощью BangPatterns.
Т.е. тут из сложного что далеко не все функции адекватно документированы с т.з. их поведения. Радует то, что с каждой новой версией ghc все больше правильных функций доступно по умолчанию.
P.S. набиралось с телефона, так что извиняюсь за опечатки, если будет возможность - поправлю.
no subject
Date: 2015-01-06 07:26 am (UTC)