thedeemon: (office)
[personal profile] thedeemon
Для одной задачки всхотелось мне сделать интерпретатор крайне простого язычка: есть память на дюжину целых чисел и AST из трех операций - сравнение двух ячеек, копирование из одной в другую и обмен значениями меж двух ячеек. Стал делать на хаскеле и сразу столкнулся с тем, что наивное решение в лоб тормозит: аналогичный код на окамле в 4 раза быстрее, а на Ди - в 10 раз. Вот так выглядит мой позор на хаскеле:


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: не, не было там бага.

Date: 2015-01-05 05:50 pm (UTC)
From: [identity profile] kodt-rsdn.livejournal.com

Наверно, там ленивость сыграла злую шутку, и хаскелл вместо вычислений рожает мега-санки, которые отрабатывают лишь в самом конце. А это удар по памяти, вот и тормоза.
Самле простое - натыкать форсирования вычислений гле только можно (на возврате из eval?) - оператор $! емнип.

Date: 2015-01-05 06:04 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
А как этот оператор с монадой ST сочетается?
Если что, STUArray - мутабельный строгий анбоксед массив, живущий в ST.
Но где-то санки могут действительно копиться, ибо аллоцируют приведенные функции как черти.

Date: 2015-01-05 06:14 pm (UTC)
From: [identity profile] permea-kra.livejournal.com
Оптимизации включать пробовали?

Date: 2015-01-05 06:15 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
-O2
GHC 7.6.3
Edited Date: 2015-01-05 06:16 pm (UTC)

Date: 2015-01-05 06:19 pm (UTC)
From: [identity profile] permea-kra.livejournal.com
Ну и да, чтобы было честно, расставьте в хаскельной версии в определении типа AST аннотации строгости и инлайна для всех интов как минимум.

upd. Конкретно, посмотрите вот эту прагму: https://downloads.haskell.org/~ghc/7.0.2/docs/html/users_guide/pragmas.html#unpack-pragma . Потому что у вас в тесте, насколько я понимаю, в АСТ инты завернуты в отдельные танки. Нужно их как миниум распаковать, иначе сравнение нечестное.
Edited Date: 2015-01-05 06:32 pm (UTC)

Date: 2015-01-05 06:43 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
data Exp = IfGt {-# UNPACK #-} !Int {-# UNPACK #-} !Int Block Block 
         | Swap {-# UNPACK #-} !Int {-# UNPACK #-} !Int 
         | Copy {-# UNPACK #-} !Int {-# UNPACK #-} !Int 


7.08 s. Крайне незначительное улучшение.

Еще пробовал unsafeRead/unsafeWrite для массива, еще около секунды дало выигрыш, тоже маловато.

Date: 2015-01-05 07:28 pm (UTC)
From: [identity profile] antilamer.livejournal.com
Я бы предложил избавиться от foldM, возможно компилятор его не специализирует и не даёт превратить остальное в цикл.

А вообще надо смотреть core.

Date: 2015-01-05 08:15 pm (UTC)
From: [identity profile] udpn.livejournal.com
Куда смотреть?

Date: 2015-01-05 08:22 pm (UTC)
From: [identity profile] qnikst (from livejournal.com)
core, промежуточный код, где используется минимум языка и на котором начинаются оптимизации. Получать по -ddump-simpl (-ddump-to-file).

(no subject)

From: [identity profile] udpn.livejournal.com - Date: 2015-01-05 08:29 pm (UTC) - Expand

(no subject)

From: [identity profile] qnikst - Date: 2015-01-05 08:33 pm (UTC) - Expand

Date: 2015-01-05 08:20 pm (UTC)
From: [identity profile] qnikst (from livejournal.com)
Не стоит использовать foldM из Control.Monad, там нужно минимум добавлять строгости.

Если использовать Data.Foldable то начинает выполняться нормально у меня за 0.05с, при том, что изначальная версия за 5с.

Ссылку на полный код можно найти на форке гиста, т.к. боюсь в жж её не запостить.

UPD. т.е. import Data.Foldable (foldlM) и использовать его вместо foldM.
так же по желанию добавить ! к аккумулятору f, т.е. f (!a, !cnt) ...

UPD2. с Data.Foldable можно оставлять вариант с fmap и убирать там явное вычисление, т.к. ghc уже справляется.
Edited Date: 2015-01-05 08:31 pm (UTC)

Date: 2015-01-05 08:51 pm (UTC)
From: [identity profile] qnikst (from livejournal.com)
Ну и как обычно, Array далеко не лучшая штука, что есть в Haskell, поэтому если существует возможность, то лучше использовать пакет vectors и подходящие вектора оттуда. В последней версии gist (https://gist.github.com/qnikst/3517def97f21555dea83) я отвоевал ещё 10% (итого 0.4с), при этом не модифицируя изначальные вектора (что в прочем на таких размерах не важно).

Я попробую еще сделать код поаккуратнее, но думаю, что вопрос в треде он снимать должен.

Date: 2015-01-06 06:41 am (UTC)
From: [identity profile] antilamer.livejournal.com
Еще чуть побыстрее становится, если Exp сделать всячески строгим (на моей машине 0.8 => 0.7с). И еще (0.53с) вот так: https://gist.github.com/jkff/7d85da1e409c8725f282

(no subject)

From: [identity profile] qnikst - Date: 2015-01-06 07:32 am (UTC) - Expand

(no subject)

From: [identity profile] antilamer.livejournal.com - Date: 2015-01-06 08:02 am (UTC) - Expand

Date: 2015-01-05 10:22 pm (UTC)
From: [identity profile] qnikst (from livejournal.com)
UPD 4. Я таки решил включить мозг и заметил, что тут в eval и evalBlock идет работа с изменяемым вектором, поэтому никакого смысла возвращать его в результате нет, т.к. у вызывающей функции уже есть этот вектор (и все изменения в нём), исправление этого недоразумения приводит к тому, что код выполняется 0.03с вместо 0.05c.

В очередной версии гиста положено.

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-06 04:28 am (UTC) - Expand

(no subject)

From: [identity profile] qnikst - Date: 2015-01-06 06:40 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-06 07:26 am (UTC) - Expand

Date: 2015-01-05 09:56 pm (UTC)
From: [identity profile] jdevelop.livejournal.com
и вот всегда так в хаскеле - пишешь красивый идиоматичный код, монады там, аппликативы

а в продакшне, матерясь, переписываешь в кашу из бангов и unsafePerformIO на MVar

Date: 2015-01-05 10:13 pm (UTC)
From: [identity profile] qnikst (from livejournal.com)
попрошу заметить, что ни один использованый функтор и монада не пострадали, а ещё вылез дополнительный бифунктор (чисто для красоты). При этом код не потерял ни долю идиоматичности.. В общем тут чисто вопрос выбора инструментов и знания основных библиотек и их особенностей.

(no subject)

From: [identity profile] stas binko - Date: 2015-01-06 12:57 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-06 04:54 am (UTC) - Expand

(no subject)

From: [identity profile] gds.livejournal.com - Date: 2015-01-06 07:41 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-06 08:21 am (UTC) - Expand

(no subject)

From: [identity profile] gds.livejournal.com - Date: 2015-01-06 08:24 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-06 08:26 am (UTC) - Expand

(no subject)

From: [identity profile] antilamer.livejournal.com - Date: 2015-01-06 05:04 am (UTC) - Expand

(no subject)

From: [identity profile] qnikst - Date: 2015-01-06 07:12 am (UTC) - Expand

Date: 2015-01-05 11:33 pm (UTC)
From: [identity profile] urod.livejournal.com
> А вот прямой перевод на Окамл, почти вчетверо быстрее

...И вдвое короче.

Date: 2015-01-06 11:02 am (UTC)
From: [identity profile] gds.livejournal.com
зоркий глаз из камлочятика заметил, что в окамле списки, а в D массивы. Как-то не ок, если целью является сравнение времени. (речь про "block = exp list".)
Edited Date: 2015-01-06 11:02 am (UTC)

Date: 2015-01-06 11:10 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Да. Я исходно не ставил задачу сделать программы 100% одинаковыми, это невозможно хотя бы в силу различия рантаймов и представлений данных, в D толком нет алгебраиков, например. Делались наивные решения - что первое приходит в голову.
Но ради эксперимента можно попробовать и с массивами на окамле.

Date: 2015-01-06 01:14 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
Кстати, окамл нынче умеет анбоксить массивы таких типов? С боксингом же ничем не лучше списка будет.

(no subject)

From: [identity profile] gds.livejournal.com - Date: 2015-01-06 01:30 pm (UTC) - Expand

(no subject)

From: [identity profile] gds.livejournal.com - Date: 2015-01-07 04:06 am (UTC) - Expand

Date: 2015-01-06 11:10 am (UTC)
From: [identity profile] geniepro.livejournal.com
Так где последние версии исходников с результатами прогонов?

Date: 2015-01-06 11:25 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Самые быстрые результаты - с ручным разворачиванием цикла.
На моем ноуте и GHC 7.6.3:

Haskell - 0.85 s
https://gist.github.com/jkff/7d85da1e409c8725f282

OCaml - 1.06 s
https://gist.github.com/thedeemon/5afe7fe65a30165aae23

(no subject)

From: [identity profile] geniepro.livejournal.com - Date: 2015-01-06 12:44 pm (UTC) - Expand

(no subject)

From: [identity profile] Игорь Петров - Date: 2015-01-06 02:53 pm (UTC) - Expand

(no subject)

From: [identity profile] antilamer.livejournal.com - Date: 2015-01-06 03:56 pm (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-06 04:18 pm (UTC) - Expand

(no subject)

From: [identity profile] Игорь Петров - Date: 2015-01-06 05:02 pm (UTC) - Expand

(no subject)

From: [identity profile] Игорь Петров - Date: 2015-01-08 05:32 pm (UTC) - Expand

Date: 2015-01-07 04:11 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Стараниями gds окамл вышел в лидеры: 0.57 с.
https://gist.github.com/thedeemon/c2c3e436d78592dabf63

Upd. ... пока я не поменял компилятор D на LDC, получив там 0.40 с.
Edited Date: 2015-01-07 07:57 am (UTC)

Date: 2015-01-07 03:14 am (UTC)
From: [identity profile] gds.livejournal.com
https://gist.github.com/gdsfh/5a08ae7b980a6070f19a
1. не создаём closure на каждый вызов eval+go -- это мне дало 1.02 -> 0.94
2. используем указание типа массива, чтобы избежать полиморфного/структурного сравнения, это дало 0.94 -> 0.57
Вот теперь збс.

Date: 2015-01-07 04:06 am (UTC)
From: [identity profile] thedeemon.livejournal.com
О, победа!
0.57 с у меня тоже.

(no subject)

From: [identity profile] gds.livejournal.com - Date: 2015-01-07 04:08 am (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-07 07:45 am (UTC) - Expand

(no subject)

From: [identity profile] gds.livejournal.com - Date: 2015-01-07 08:09 am (UTC) - Expand

(no subject)

From: [identity profile] Игорь Петров - Date: 2015-01-08 05:36 pm (UTC) - Expand

(no subject)

From: [identity profile] thedeemon.livejournal.com - Date: 2015-01-08 06:02 pm (UTC) - Expand

Date: 2015-03-15 10:40 am (UTC)
From: [identity profile] eugene smolanka (from livejournal.com)
Вот еще одна попытка. https://gist.github.com/esmolanka/1cfa9e1c44d71072517a

Original (https://gist.github.com/thedeemon/d9dfe4982ab6c5e68854) - 3.855s
Fast (https://gist.github.com/jkff/7d85da1e409c8725f282) - 0.595s
This one - 0.375s

Profile

thedeemon: (Default)
Dmitry Popov

December 2025

S M T W T F S
 12 3456
789101112 13
14151617181920
21222324252627
28293031   

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 28th, 2026 08:02 pm
Powered by Dreamwidth Studios