| 1 2 3 4 5 6 |
| Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 21.02.08 23:14 | ||
| Оценка: | 118 (11) +1 | ||
| Disclaimer: всего лишь собственные наблюдения, захотелось поделиться. Может кому интересно будет. Haskell заработал репутацию чрезвычайно тормознутого языка. Ленивость, добавляющая свой оверхед к любой операции, все значения в хипе, иммутабельность значений — все это, конечно, скорости не добавляет. Понадобилось решить одну задачку, связанную с разбором гибридного бинарно-текстового формата. Поскольку смысл этого решения — всего лишь упрощение собственной повседневной работы, позволил себе повыбирать средство реализации. В том числе и по быстродействию. И в велосипедописании потренироваться. И вот тут Haskell очень сильно удивил. Поскольку много кода дублировать на разных языках ой как не хотелось, тест сведен к простой задаче: парсинг потока double'ов. В качестве источника данных — текстовый файл, содержащий 5 000 000 чисел в диапазоне [-10 000; 10 000], размером около 82 мб (сгенереный таким вот C#-кодом):
А вот код парсера на Haskell. Код наверняка далек от идеального и избыточен (но и писался он не только для такой простой задачи). Кстати, если кто из здешних знатоков Haskell найдет что покритиковать — буду грейтфул Компилятор GHC 6.8.2, компиляция, естественно, с оптимизацией:
От имени C++ выступает boost::spirit, как инструмент, дающий в руки C++-программиста абстракции, примерно идентичные ФП-шным комбинаторным парсерам. Вот такой (все просто, т.к. используется встроенный спиритовский парсер real_p)
Компилятор — Visual C++ Express 2008. Все опции оптимизации на максимум — Full optimization, inline any suitable, favor fast code, link-time code generation. Результат работы (Core2Duo 3,12 ггц) Haskell — ~1.56 секунды. 52,5 мб/сек. C++ (spirit) — ~1.88 секунды. 44 мб/сек. Правда, если рантайм сменить с DLL на статически линкуемый, то ~1.65 секунды. Вот так. Haskell вровень с C++, и даже опережает. Хотел привести еще то же решение на F#, но если не скатываться к традиционному императивному решению (а зачем тогда F# нужен?), быстрее 10 сек. пока не получилось. Вообще, старичок двухплюсатый все равно на высоте — если все переписать вручную (код тупой, приводить не охота), отрабатывает за 0,45 сек. Только ведь в более сложном случае вероятность такого ручного расписывания невелика. Ну а по выразительности c Haskell сложно что-либо сравнивать. Монады — вообще чертовски интересная штука. Этакий очень абстрактный интерфейс, который, если придумать, как пристегнуть к своей задаче, дает бонус в виде стандартных библиотечных функций, ориентированных на работу с монадами. Например, из кода выше:
|
| Re: Haskell :: не такой тормоз, как кажется | |
| От: | Аноним 734 | ||
| Дата: | 22.02.08 05:22 |
| Здравствуйте, Schade: вы сравниваете не хаскель и с++, а свой парсер со спиритом. здесь же на rsdn было показано, что парсер на с# в 10 раз быстрей спирита. сам по себе спирит — достаточно тормозная штуковина |
| Re: Haskell :: не такой тормоз, как кажется | |
| От: | DmitryMe | ||
| Дата: | 22.02.08 07:20 |
| > И в велосипедописании потренироваться. И вот тут Haskell очень сильно удивил. да уж навелосипедировал от души Есть еще вот такой ранкинг гейм: http://shootout.alioth.debian.org/gp4/benchmark.php?test=all&lang=all так что с Haskell действительно не все так плохо |
| Re: Haskell :: не такой тормоз, как кажется | |
| От: | lomeo | ||
| Дата: | 22.02.08 07:40 | ||
| Оценка: | 6 (1) | ||
| Здравствуйте, Schade, Вы писали: Симпатичный код, приятно читать. Может быть ты не знал (забыл?), но ok@(Pass _ _) можно записать как ok@Pass{}. |
| Re[2]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 22.02.08 07:42 |
| Здравствуйте, Аноним, Вы писали: А>вы сравниваете не хаскель и с++, а свой парсер со спиритом. Ну, в общем, да. Вроде бы прямым текстом об этом и пишу. И причина указана: более-менее одинаковый подход к решению задачи (насколько это вообще возможно в случае C++ и Haskell) A>здесь же на rsdn было показано, что парсер на с# в 10 раз быстрей спирита. сам по себе спирит — достаточно тормозная штуковина Ссылку можно? Интересно поглядеть. Вообще, я привел результат на C++ врукопашную (только код не приводил |
| Re[2]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 22.02.08 07:53 |
| Здравствуйте, DmitryMe, Вы писали: DM>Есть еще вот такой ранкинг гейм: DM>http://shootout.alioth.debian.org/gp4/benchmark.php?test=all&lang=all Знаем-с. Могу ошибаться, но по-моему в тех ранкигнах Haskell выезжает в основном на использовании более продвинутых алгоритмов, сложных в реализации на более низкоуровневых языках. Мне же было интересно сравнить на более-менее одинаковых алгоритмах, собственно почему и сравнивал со спиритом. В некотором роде этот пост — ответ на заявление Автор: VladD2 , что этому компилятору уже ничего не поможет.Дата: 20.02.08 |
| Re[3]: Haskell :: не такой тормоз, как кажется | |
| От: | Аноним 213 | ||
| Дата: | 22.02.08 08:56 |
| Здравствуйте, Schade, Вы писали: А>>вы сравниваете не хаскель и с++, а свой парсер со спиритом. S>Ну, в общем, да. Вроде бы прямым текстом об этом и пишу. И причина указана: более-менее одинаковый подход к решению задачи (насколько это вообще возможно в случае C++ и Haskell) А если то же самое эксперимента ради на Parsec переписать? |
| Re[4]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 22.02.08 11:46 |
| Здравствуйте, Аноним, Вы писали: А>А если то же самое эксперимента ради на Parsec переписать? Надо попробовать. Я правда как-то сходу им не проникся, там можно без промежуточных списков обойтись? А то на deforestation надейся, а сам не плошай — со списками такая тормозуха получается. |
| Re: Haskell :: не такой тормоз, как кажется | |
| От: | http://migmit.vox.com/ гость | ||
| Дата: | 22.02.08 17:42 | ||
| Оценка: | 10 (1) | ||
| Симпатично. Некоторое количество замечаний таки набралось. S>instance Functor (ParseResult s) where S> fmap f (Pass s a) = Pass s (f a) S> fmap _ (Fail s) = Fail s Зачем? Мы используем этот fmap только в одном месте — см. ниже. S>instance Functor (Parser s) where S> fmap f (Parser p) = Parser $ fmap f . p Тут проще написать ... where fmap = liftM. И всё. И не требуется instance Functor (ParseResult s). S>instance Monad (Parser s) where S> return a = Parser $ \s -> Pass s a S> (Parser p) >>= f = Parser $ \s -> S> case p s of S> Pass s' a' -> runParser (f a') s' S> Fail s -> Fail s Очень не вредно объявить fail _ = Parser $ \s -> Fail s S>newtype WriterP w m a = WriterP { runWriterP :: w -> m (w, a) } А не проще воспользоваться mtl-ным StateT? Он ведь ровно так и определяется, с точностью до порядка элементов в паре. S>instance Monad m => Monad (WriterP w m) where ... S>instance MonadTrans (WriterP w) where ... S>instance MonadPlus m => MonadPlus (WriterP w m) where ... S>instance Functor m => Functor (WriterP w m) where ... Всё это мы получаем от StateT бесплатно, кроме последнего — в mtl там стоит контекст (Monad m) — но нам ведь это по фигу? Кстати, это, похоже, баг в mtl, надо, действительно, Functor. S>class Monad m => ParseMonad m a where S> p_take :: m a Очень правильно, интерфейс монады надо определять в классе. S>instance ParseMonad m a => ParseMonad (WriterP w m) a where S> p_take = lift p_take Ну, понятно, StateT вместо WriterP. Дальше заменять не буду, и так понятно. Или можно просто сказать type WriterP w m a = StateT w m a. S>p_pred :: (ParseMonad p a, MonadPlus p) => (a -> Bool) -> p a S>p_pred f = do S> a <- p_take S> if f a then return a else mzero Последнюю строчку я бы разбил на две: guard $ f a return a S>many :: (Functor p, MonadPlus p) => p a -> p () S>many p = mn S> where mn = do S> a <- p_try p S> case a of {Just _ -> mn; _ -> return () } Может, просто написать many p = many1 p `mplus` return ()? Или, если уж на то пошло, сделать many :: p a -> p [a]; many p = many1 p `mplus` return [] S>many1 p = p >> many p Соответственно, many1 p = liftM2 ( S>writing p = WriterP $ \(!w) -> do S> a <- p S> return (a_append a w, ()) Уй! Можно уровнем повыше работать. Во-1, если уж (!), то импортировать Control.Monad.State.Strict. Во-2, переписать таким образом: writing p = do {a <- lift p; modify $ a_append a; return ()}. Кстати, а почему мы обязательно возращаем ()? S>writingF f p = WriterP $ \(!w) -> do S> a <- p S> return (f a w, ()) Аналогично: writingF f p = do {a <- lift p; modify $ f a; return ()}; кстати, тогда не вредно бы определить выше writing как writingF a_append. S>wrRun :: (Monad p, Functor p) => WriterP w p x -> w -> p w S>wrRun (WriterP p) w = fmap fst $ p w wrRun = execStateT S>wrUnwrap :: (Functor p, Accumulator w e) => WriterP w p x -> (w -> a) -> p a Ценой замены Functor на Monad, получаем wrUnwrap p f = liftM f $ execStateT p a_empty S>digit = fmap digitToInt $ p_pred isDigit ИМХО — только ИМХО — для монад лучше читается liftM, а не fmap (хотя это одно и то же). S>data IntAcc = IntAcc { getInt :: {-# UNPACK #-} !Double } А почему не newtype? S>measured act = do S> let getSeconds t = fromIntegral (tdPicosec t) / 1000000000000.0 + fromIntegral (tdSec t) S> start <- getClockTime S> !r <- act S> end <- getClockTime S> let delta = getSeconds $ diffClockTimes end start S> return (r, delta) Чего-то мне в голову стукнуло, что bang patterns внутри do-блоков ничего не делают. Не ручаюсь, что что-нибудь из этого не вызовет падения производительности. Кстати, я слышал, что с какими-то ghc поставлялась mtl, откомпилированная без оптимизации — что, естественно, замедляло работу. |
| Re[2]: Haskell :: не такой тормоз, как кажется | |
| От: | http://migmit.vox.com/ гость | ||
| Дата: | 22.02.08 17:48 |
| Блин. Дико извиняюсь, (:) заменилось на корявый смайлик. |
| Re[2]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 22.02.08 19:07 |
| Здравствуйте, http://migmit.vox.com/, Вы писали: HMV>Симпатично. Некоторое количество замечаний таки набралось. Замечательно, что набралось. S>>instance Functor (ParseResult s) where HMV>Зачем? Мы используем этот fmap только в одном месте — см. ниже. Ну, в конечном итоге, мы же все равно вызываем runParser и получаем ParseResult. Почему бы и не оставить? Хотя в принципе да, liftM достаточно. S>>instance Functor (Parser s) where S>> fmap f (Parser p) = Parser $ fmap f . p HMV>Тут проще написать ... where fmap = liftM. И всё. И не требуется instance Functor (ParseResult s). +1. Сам заметил уже после. S>>instance Monad (Parser s) where HMV>Очень не вредно объявить fail _ = Parser $ \s -> Fail s +1 S>>newtype WriterP w m a = WriterP { runWriterP :: w -> m (w, a) } HMV>А не проще воспользоваться mtl-ным StateT? Он ведь ровно так и определяется, с точностью до порядка элементов в паре. S>>instance Monad m => Monad (WriterP w m) where ... S>>instance MonadTrans (WriterP w) where ... S>>instance MonadPlus m => MonadPlus (WriterP w m) where ... S>>instance Functor m => Functor (WriterP w m) where ... HMV>Всё это мы получаем от StateT бесплатно, кроме последнего — в mtl там стоит контекст (Monad m) — но нам ведь это по фигу? Кстати, это, похоже, баг в mtl, надо, действительно, Functor. В общем-то, сначала проглядел наличие в стандартной библиотеке strict state S>>many :: (Functor p, MonadPlus p) => p a -> p () S>>many p = mn S>> where mn = do S>> a <- p_try p S>> case a of {Just _ -> mn; _ -> return () } HMV>Может, просто написать many p = many1 p `mplus` return ()? Или, если уж на то пошло, сделать many :: p a -> p [a]; many p = many1 p `mplus` return [] S>>many1 p = p >> many p HMV>Соответственно, many1 p = liftM2 ( Что касается первого варианта: many p = many1 p `mplus` return () — ленивость языка, конечно, позволяет уделять меньше внимания преобразованию наивной рекурсии в хвостовую. Но тут не спасает — на сколько-нибудть серьезных объемах данных получаем stack overflow. many :: p a -> p [a] — это самое очевидное, наглядное, удобное в использовании и легкочитаемое решение. Но, как часто бывает в таких случаях, полный (_|_) bottom по части производительности. А если в результате нужен все-таки список, можем определить
Хотя, конечно, не идеально. Хорошо бы без reverse обойтись. S>>writing p = WriterP $ \(!w) -> do S>> a <- p S>> return (a_append a w, ()) HMV>Уй! Можно уровнем повыше работать. Во-1, если уж (!), то импортировать Control.Monad.State.Strict. Во-2, переписать таким образом: writing p = do {a <- lift p; modify $ a_append a; return ()}. Кстати, а почему мы обязательно возращаем ()? (!) — это потому что с ленивым Хаскелем именно в этом месте нужно обойтись со всей строгостью HMV>Аналогично: writingF f p = do {a <- lift p; modify $ f a; return ()}; кстати, тогда не вредно бы определить выше writing как writingF a_append. +1 HMV>wrRun = execStateT +1 HMV>Ценой замены Functor на Monad, получаем wrUnwrap p f = liftM f $ execStateT p a_empty +1 S>>digit = fmap digitToInt $ p_pred isDigit HMV>ИМХО — только ИМХО — для монад лучше читается liftM, а не fmap (хотя это одно и то же). S>>data IntAcc = IntAcc { getInt :: {-# UNPACK #-} !Double } HMV>А почему не newtype? Для newtype не позволяет указать strictness annotation. Хотя да, в случае с newtype компилятор, похоже, и сам выполняет UNPACK, даже процентов на 5 в итоге быстрее получается. Что-то я в прошлый раз не так делал, что с newtype получил тормоза. |
| Re[4]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 23.02.08 12:36 |
| Здравствуйте, Аноним, Вы писали: А>А если то же самое эксперимента ради на Parsec переписать?
Работает только при относительно небольшом объеме данных. С 1 000 000 чисел не справляется — долгое пыхтение, затем stack overflow. Явный признак чрезмерной ленивости. Увеличиваем стек — отрабатывает со скоростью примерно 1,33 мб/сек, в 35-37 раз медленнее моего примера. Беда в том, что в чужом коде не всегда легко понять, в каком месте прищемить хаскелю его ленивость. Ну и конечно просто убивает традиция haskell community писать так, как будто данных всегда будет заведомо мало, а память и стек бесконечны. Наверное можно переписать так, чтобы отрабатывало нормально, но все равно будет медленнее. Хотя бы из-за того, что входной поток может быть только списком. |
| Re[5]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 26.02.08 21:06 |
| [] Опп... неспортивно обошелся с парсеком... Во первых, списки не настолько дешевы, чтобы все время работы держать ссылку на голову списка Во вторых, явно нужно добавить строгости:
Так на основном тестовом файле отрабатывает. Но скорость... 136 секунд. В 87 раз медленнее. |
| Re[6]: Haskell :: не такой тормоз, как кажется | |
| От: | Аноним 481 | ||
| Дата: | 27.02.08 13:17 |
| Здравствуйте, Schade, Вы писали: S>Так на основном тестовом файле отрабатывает. Но скорость... Действительно, быстро доработать Parsec чтобы тот парсил BypeString не получается... уж чень он завязан на String. Ну а если взять например, Text.ParserCombinators.ReadP.ByteString ? Хотя он не его набор готовых примитивов не столь разнообразен, как парсековский |
| Re: Haskell :: не такой тормоз, как кажется | |
| От: | D. Mon | ||
| Дата: | 29.02.08 17:47 | ||
| Оценка: | 15 (1) | ||
| Ради интереса изобразил нечто подобное на OCaml с использованием enumerations из ExtLib. На Core2Duo 2.0 GHz получается всего 3,8 МБ/с..
|
| Re[2]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 02.03.08 11:36 |
| Здравствуйте, D. Mon, Вы писали: DM>Ради интереса изобразил нечто подобное на OCaml с использованием enumerations из ExtLib. DM>На Core2Duo 2.0 GHz получается всего 3,8 МБ/с.. Вообще странно конечно. Вроде бы Камл считается шустрым. У меня отработал немного быстрее — 5,4 Мб/с. Но вот что интересно — подобное на F# оказалось быстрее (причем F# с участием его новой фичи — computation expressions aka monads). [страшный нечитаемый ужас]
[/страшный нечитаемый ужас] Отрабатывает со скоростью 7,3 Мб/с, процентов на 40 шустрее. И все равно отставание от Хаскеля катастрофическое — в 7 раз |
| Re[3]: Haskell :: не такой тормоз, как кажется | |
| От: | D. Mon | ||
| Дата: | 15.03.08 09:06 |
Заменил в своем OCaml коде монады на то, что они эмулировали, скорость возросла в 4,3 раза до 16 МБ/с.
|
| Re: Haskell :: не такой тормоз, как кажется | |
| От: | Basil B | ||
| Дата: | 15.03.08 09:33 |
| Здравствуйте, Schade, Вы писали: S>Ну а по выразительности c Haskell сложно что-либо сравнивать. ? На плюсах получилось гораздо короче и яснее, имхо. |
| Re[2]: Haskell :: не такой тормоз, как кажется | |
| От: | Schade | ||
| Дата: | 15.03.08 09:54 |
| Здравствуйте, Basil B, Вы писали: BB>? На плюсах получилось гораздо короче и яснее, имхо. Ну, вообще-то, чтобы сравнивать приведенный Haskell-код с плюсами к плюсовому коду нужно прибавить еще весь boost::spirit. И тут от краткости и ясности камня на камне не останется. |
| Re[3]: Haskell :: не такой тормоз, как кажется | |
| От: | dr.Chaos | ||
| Дата: | 15.03.08 11:18 |
| Schade wrote: > > Ну, вообще-то, чтобы сравнивать приведенный Haskell-код с плюсами к > плюсовому коду нужно прибавить еще весь boost::spirit. И тут от краткости > и ясности камня на камне не останется. Ты, кстати, с Parsec'ом 3-й версии не пробовал? Posted via RSDN NNTP Server 2.1 beta Побеждающий других — силен, Побеждающий себя — Могущественен. Лао Цзы |
| 1 2 3 4 5 6 |