LINUX.ORG.RU

Научите правильно готовить монады через CPS

 ,


4

4

Тут будет много букв, я морально готовился месяц, поэтому за объем заранее извиняйте. Сразу скажу - на ассемблерах я не писатель, и цель треда - не обсуждение дизайнерских решений, а производительность монад. Суть всплывёт ближе к концу (я надеюсь).

Представим, что мы моделируем некий процессор, у которого есть простой набор инструкций. С инструкций и начнем:

data Register = R1 | R2 | R3 | R4 | R5 | R6
                deriving (Show)
     
data Operator = ADD | SUB | MUL | DIV
              | LESS | EQUAL | AND | OR | NOT
              | MOV
              | JMT | JMF | JMP
              | PRN | NOP
                deriving (Show)

class OperandClass a where
   toOperand :: a -> Operand

instance OperandClass Register where
   toOperand = R

instance OperandClass Operand where
   toOperand = id

instance OperandClass () where
   toOperand _ = N

data Operand = R !Register | V !Double | I !Int | N
               deriving (Show)

type Instruction = (Operator, Operand, Operand) 

Иными словами, всё очень просто: инструкция - это кортеж оператора и операндов, операндом могут быть регистр, значение, или ничего.

Чтобы кодить на нашем псевдоассемблере, потребуется таки простенький компилятор. Цель его - записать все инструкции в списочек.

-- | Monad for programming instructions 

type ASM a = State [Instruction] a

compile :: ASM a -> [Instruction]
compile c = execState c []

op :: (OperandClass s, OperandClass d) => Operator -> s -> d -> ASM ()
op cmd src dst = modify $ \s -> s ++ [(cmd, toOperand src, toOperand dst)]

pos :: ASM Int
pos = liftM length get

nop :: ASM Int
nop = do { p <- pos; op NOP () (); return p}

putOp :: (OperandClass s, OperandClass d) => Int -> Operator -> s -> d -> ASM ()
putOp p cmd src dst = do
    let instr = (cmd, toOperand src, toOperand dst)
    (before,after) <- liftM (splitAt p) get 
    put $ before ++ instr : tail after

Тут, опять же, ничего сложного. Для наглядности, немного забегая вперед, приведу пример кода на этом «ассемблере». Данный код считает квадратный корень заданного числа методом Герона с заданным приближением (числом шагов):

-- | Heron's method to calculate square root
-- Inputs:  r1 - value to calculate square root from
--          r2 - number of iterations
-- Outputs: r6 - output value
heron :: ASM ()
heron = do
   op MOV (V 1) R5
   op MOV (V 0) R3
   iterStart <- pos
   op MOV R3 R4
   op EQUAL R2 R4
   ifFalse <- nop
   op MOV R1 R6
   op DIV R5 R6
   op ADD R5 R6
   op MUL (V 0.5) R6
   op MOV R6 R5
   op ADD (V 1) R3
   op JMP (I iterStart) ()
   loopEnd <- pos
   putOp ifFalse JMT R4 (I loopEnd)
   op PRN R6 ()

Надеюсь, тут становится ясна необходимость в действиях pos, nop, и putOp, описанных ранее - их суть в расстановке меток и замене пустышек на инструкции, привязанные к меткам. Не самое элегантное, но, как по мне, вполне решение для организации циклических конструкций.

Идём дальше. Чтобы это всё выполнять, нужна ещё одна монада:

-- | Monad for executing instructions

data Registers = Registers
               { r1 :: !Double
               , r2 :: !Double
               , r3 :: !Double
               , r4 :: !Double
               , r5 :: !Double
               , r6 :: !Double
               } deriving (Show)

initialRs :: Registers
{-# INLINE initialRs #-}
initialRs = Registers
            { r1 = 0
            , r2 = 0
            , r3 = 0
            , r4 = 0
            , r5 = 0
            , r6 = 0
            }

type CPU a = StateT Registers IO a

execute ::Registers -> [Instruction] -> IO Registers
execute rs code = execStateT (exec code) rs
  where
   {-# INLINE exec #-}
   exec ((JMP, I pos, _    ):is) = {-# SCC "JMP" #-} exec $! drop pos code
   exec ((JMF,   reg, I pos):is) = {-# SCC "JMF" #-} readVal reg >>= \v ->
                                                     exec $! if toBool v
                                                             then is
                                                             else drop pos code
   exec ((JMT,   reg, I pos):is) = {-# SCC "JMT" #-} readVal reg >>= \v ->
                                                     exec $! if toBool v
                                                             then drop pos code
                                                             else is
   exec ((ins,   src,   dst):is) = {-# SCC "OP"  #-} execOP ins src dst >> exec is
   exec []                       = return ()

execOP :: Operator -> Operand -> Operand -> CPU ()
{-# INLINE execOP #-}
execOP ADD   src dst = {-# SCC "ADD"   #-} arith ADD   src dst
execOP SUB   src dst = {-# SCC "SUB"   #-} arith SUB   src dst
execOP MUL   src dst = {-# SCC "MUL"   #-} arith MUL   src dst
execOP DIV   src dst = {-# SCC "DIV"   #-} arith DIV   src dst
execOP LESS  src dst = {-# SCC "LESS"  #-} logic LESS  src dst
execOP EQUAL src dst = {-# SCC "EQUAL" #-} logic EQUAL src dst
execOP AND   src dst = {-# SCC "AND"   #-} logic AND   src dst
execOP OR    src dst = {-# SCC "OR"    #-} logic OR    src dst
execOP NOT   src dst = {-# SCC "NOT"   #-} logic NOT   src dst
execOP MOV   src dst = {-# SCC "MOV"   #-} readVal src >>= \v -> putVal dst $! v
execOP PRN   src _   = {-# SCC "PRN"   #-} readVal src >>= \v -> liftIO $ print v 

arith :: Operator -> Operand -> Operand -> CPU ()
{-# INLINE arith #-}
arith op src dst = do
    v1 <- readVal src
    v2 <- readVal dst
    case op of
       ADD -> putVal dst $! v2 + v1
       SUB -> putVal dst $! v2 - v1
       MUL -> putVal dst $! v2 * v1
       DIV -> putVal dst $! v2 / v1

logic :: Operator -> Operand -> Operand -> CPU ()
{-# INLINE logic #-}
logic op src dst = do
     v1 <- readVal src
     v2 <- readVal dst
     case op of
        LESS  -> putVal dst $! fromBool $ v2 <  v1
        EQUAL -> putVal dst $! fromBool $ v2 == v1
        AND   -> putVal dst $! fromBool $ toBool v1 && toBool v2
        OR    -> putVal dst $! fromBool $ toBool v1 && toBool v2
        NOT   -> putVal dst $! fromBool . not . toBool $ v1

fromBool :: Bool -> Double
{-# INLINE fromBool #-}
fromBool True  = 1
fromBool False = 0

toBool :: Double -> Bool
{-# INLINE toBool #-}
toBool 0 = False
toBool _ = True

readVal :: Operand -> CPU Double
{-# INLINE readVal #-}
readVal (R R1) = gets r1
readVal (R R2) = gets r2
readVal (R R3) = gets r3
readVal (R R4) = gets r4
readVal (R R5) = gets r5
readVal (R R6) = gets r6
readVal (V v)  = return v

putVal :: Operand -> Double -> CPU ()
{-# INLINE putVal #-}
putVal (R R1) v = modify $ \s -> s { r1 = v }
putVal (R R2) v = modify $ \s -> s { r2 = v }
putVal (R R3) v = modify $ \s -> s { r3 = v }
putVal (R R4) v = modify $ \s -> s { r4 = v }
putVal (R R5) v = modify $ \s -> s { r5 = v }
putVal (R R6) v = modify $ \s -> s { r6 = v }

Тут кода уже побольше, но, опять же, ничего сложного - мы просто проходимся по списку инструкций и интерпретируем каждую, выполняя соответствующие действия. Состояние нашего игрушечного процессора - шесть дабловых регистров.

Вот и всё!

Полная версия кода доступна на Гитхабе.

С файликом, загруженным в ghci, можно поиграться, например, так:

let h = compile heron
execute (initialRs {r1 = 25, r2 = 4}) h

5.015247601944898 Registers {r1 = 25.0, r2 = 4.0, r3 = 4.0, r4 = 1.0, r5 = 5.015247601944898, r6 = 5.015247601944898}

Но самое важное для меня в данной ситуации - это время, за которое наш компьютер посчитает 10 000 корней. Входные данные и скрипт для запуска есть в репозитории:

$ ghc --make -O3 Asm.hs
$ ./measure.sh

Судя по результатам профайлера, основное время мы таки проводим в нашем процессоре:

COST CENTRE MODULE    %time %alloc

OP          Main       33.6   34.2
PRN         Main       21.4   23.0
READ        Main       16.4   12.8
MOV         Main        8.8   13.2
ADD         Main        5.4    6.7
DIV         Main        3.3    3.7
EQUAL       Main        3.1    3.4
MUL         Main        2.7    3.0
JMT         Main        2.3    0.0
JMP         Main        1.9    0.0


                                                                  individual     inherited
COST CENTRE  MODULE                             no.     entries  %time %alloc   %time %alloc

MAIN         MAIN                                52           0    0.7    0.0   100.0  100.0
 JMT         Main                               113      200000    2.3    0.0    80.7   85.4
  OP         Main                               114     1610000   32.8   33.3    78.3   85.4
   PRN       Main                               121       10000   21.4   23.0    21.4   23.0
   EQUAL     Main                               120      200000    2.7    3.2     2.7    3.2
   JMP       Main                               119      200000    1.9    0.0     1.9    0.0
   MUL       Main                               118      200000    2.7    3.0     2.7    3.0
   ADD       Main                               117      400000    5.4    6.7     5.4    6.7
   DIV       Main                               116      200000    3.3    3.7     3.3    3.7
   MOV       Main                               115      600000    8.2   12.6     8.2   12.6
 EQUAL       Main                               111           0    0.4    0.2     0.4    0.2
 MOV         Main                               108           0    0.6    0.6     0.6    0.6
 OP          Main                               106       40000    0.8    0.9     0.8    0.9
  JMT        Main                               112       10000    0.0    0.0     0.0    0.0
  EQUAL      Main                               110       10000    0.0    0.0     0.0    0.0
  MOV        Main                               107       30000    0.0    0.0     0.0    0.0
 ITER        Main                               105       10000    0.4    0.1     0.4    0.1
 READ        Main                               104           1   16.4   12.8    16.4   12.8
 CAF         Main                               103           0    0.0    0.0     0.0    0.0
  READ       Main                               109           0    0.0    0.0     0.0    0.0

Т.о., исполнение собственно операций (кроме ввода-вывода) занимает около 70% всего времени.

Внимание, суть!. Интересно, как зависит время выполнения от способа организации (или, если хотите, реализации) главной монады в тесте - CPU. Самое простое для начала - попробовать готовые реализации.

Мои результаты:

  • 0.37с - для монады CPU, реализованной поверх пакета transformers
  • 0.39c - для mtl
  • 0.42c - для contstuff

ХаскельВики рекомендует путь джедаев - ручной анроллинг стэков трансформеров и(ли) переход на CPS. Переход на CPS - вообще вещь легендарная, в иных историях успеха оно ускоряет код раз так в 4-8. Проверим:

newtype CPU a = CPU { runCPU :: forall r. Registers -> (a -> Registers -> IO r) -> IO r }

instance Monad CPU where
   return = retCPU
   (>>=)  = bindCPU

instance MonadIO CPU where
   liftIO = lioCPU

retCPU :: a -> CPU a
{-# INLINE retCPU #-}
retCPU x = CPU $ \s k -> k x s

bindCPU :: CPU a -> (a -> CPU b) -> CPU b
{-# INLINE bindCPU #-}
bindCPU (CPU m) f = CPU $ \s0 k -> m s0 $ \a s1 -> runCPU (f a) s1 k

lioCPU :: IO a -> CPU a
{-# INLINE lioCPU #-}
lioCPU f = CPU $ \s k -> f >>= \x -> k x s

get :: CPU Registers
{-# INLINE get #-}
get = CPU $ \s k -> k s s

gets :: (Registers -> a) -> CPU a
{-# INLINE gets #-}
gets f = get >>= \s -> return $! f s

put :: Registers -> CPU ()
{-# INLINE put #-}
put s = CPU $ \_ k -> k () s

modify :: (Registers -> Registers) -> CPU ()
{-# INLINE modify #-}
modify f = get >>= \s -> let s' = f s in put $! s'

Хотите верьте, хотите нет - такой вариант показал результат в 0.41c. Я в печали.

При ручном анроллинге (без CPS):

newtype CPU a = CPU { runCPU :: Registers -> IO (Registers, a) }

instance Monad CPU where
   return = retCPU
   (>>=)  = bindCPU

instance MonadIO CPU where
   liftIO f = CPU $ \s -> f >>= \x -> return (s, x)

retCPU :: a -> CPU a
{-# INLINE retCPU #-}
retCPU x = CPU $ \s -> return (s, x)

bindCPU :: CPU a -> (a -> CPU b) -> CPU b
{-# INLINE bindCPU #-}
bindCPU m f = CPU $ \s -> do (s', a) <- runCPU m s
                             runCPU (f a) s'

get :: CPU Registers
{-# INLINE get #-}
get = CPU $ \s -> return $! (s, s)

gets :: (Registers -> a) -> CPU a
{-# INLINE gets #-}
gets f = get >>= \s -> return $! f s

put :: Registers -> CPU ()
{-# INLINE put #-}
put s = CPU $ \_ -> return (s, ())

modify :: (Registers -> Registers) -> CPU ()
{-# INLINE modify #-}
modify f = get >>= \s -> let s' = f s in put $! s'

...всё несколько лучше, удалось получить 0.34с, но это всё равно не тот прирост, который я ожидал.

Данные эксперименты и опыт коллег, которые-делали-почти-то-же-самое-и-у-них-получилось, наталкивают на мысли, что что-то где-то пошло не так. Поэтому мой вопрос светлейшим умам форума сего - как воспроизвести те впечатляющие результаты с анроллингом и CPS? Какую деталь я мог упустить?

Для вашего удобства, весь код в его текущем состоянии выложен на Гитхабе, каждый вариант в своей ветке.

Простите ещё раз за такую простыню и неровный почерк. Всем любви!

★★★★★

«Это завело меня в тупик, и я начал пить»

yoghurt ★★★★★
() автор топика

Ты делаешь правильные выводы: встань и пиши на ассме. Забудь о монадах и хаскелле.

gh0stwizard ★★★★★
()

нехочу объяснять очевидное,но вроде ты именно этого не понимаешь;
ты на каком языке пишешь?
суть в том что «архитектура логики» должна быть «оптимизирована» для языка
тоесть если это СИ-нельзя использовать строки
если С++ - нельзя использовать каллбеки незная как они реализованы в конкретном компиляторе
...
я не писал на «Haskell» и не знаю слабых мест,но я писал подобное на java-там я сделал каждую команду класом/методом(скопировав структуру конечного устройства),и свел все к прямой трансляции комманд в исполняемый код-загруженных классов(тоесть убрав процесс эмуляции а реализовал непосредственно логику в джаве чем ускорил процесс работы многократно)

тоесть я к тому что тебе нужно знать четкую модель логики и скопировать ее,сделав десятки методов под различные комбинации вызовов(у тебя всего десяток операторов,это несколько сотен комбинаций всего)

но твой код-он «идеальное исполнение абстрактной схемы проекта без оптимизации»,тоесть да тебе нужно написать еще пару сотен таких страниц-тогда оно заработает нормально,иначе покупай кластер восьмиядерников(что щас очень популярно-вместо грамотных оптимизаций и разложения логики на дополнительные схемы-покупают еще три кластера в пару этажей,увольняя всех работающих над проектом(привет яндексу))

anonymous
()

Жесть. Надо будет как-нибудь сесть и попытаться целиком все это прочитать

kravich ★★★★
()

Зачем ты заинлайнил всё подряд?

Твой код из cps бранча

~/devel/hs/haskell-asm-benchmark> ./measure.sh         
0.39

Тот же код без INLINE

~/devel/hs/haskell-asm-benchmark> ./measure.sh         
0.32

Навскидку больше сейчас ничего не скажу, надо проспаться.

P.S. Добавь instance Applicative CPU, а то цомпилер ругается %)

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)

рекомендует путь джедаев - ручной анроллинг стэков трансформеров и(ли) переход на CPS.

Перевод кода в CPS форму _никогда_ его ускорить не сможет - наоборот, вполне легко словить замедление в 3-4 раза.

Здесь суть просто в том, что монады тормозные сами по себе (по-этому переход к тормозному CPS может, теоретически, дать профит). Выкинь монады и все будет летать (да и код станет значительно проще и короче).

anonymous
()
Ответ на: комментарий от anonymous

монады тормозные сами по себе

Можно подробностей? Я знаю, что стэк трансформеров может тормозить, но насчёт одной монады (хотя бы тот же Reader) я не уверен.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Зачем ты заинлайнил всё подряд?

«However stacked monad transformers do not inline well and the library is in need of an optimization pass» (c) HaskellWiki

У себя в настоящем проекте я получил очень хороший прирост, заинлайнив некоторые функции, тут же по старой памяти всё подряд пустил.

Тот же код без INLINE

Интересно, это вообще без всех INLINE-ов?

P.S. Добавь instance Applicative CPU, а то цомпилер ругается %)

В какой версии? У меня не ругается

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Перевод кода в CPS форму _никогда_ его ускорить не сможет - наоборот, вполне легко словить замедление в 3-4 раза.

Но у пацанов же работает! (1, 2)

Выкинь монады и все будет летать (да и код станет значительно проще и короче).

Кстати, наверное, стоит попробовать. Изначально в задаче, для которой я смоделировал эту, я использовал много монадических штучек (всякие там forM_, mapM_ - с действиями, завязанные на окружении), теперь же со сменой парадигмы нужда в них почти исчезла.

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

Тот же код без INLINE

Интересно, это вообще без всех INLINE-ов?

Ну типа да.

В какой версии? У меня не ругается

7.8.4. Начиная с 7.10 будет class Applicative m => Monad m. И ещё собираются в некоторых функциях из Prelude заменить списки на Foldable/Traversable.

hateyoufeel ★★★★★
()
op cmd src dst = modify $ \s -> s ++ [(cmd, toOperand src, toOperand dst)]

ну ёлки-палки :(

P.S. -O3 не даёт вообще ничего, смотреть надо -O2 и -O1, из-за меньшего инлайна второе может даже бонус давать на некоторых задачах.

P.P.S. попозже посмотрю подробно.

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

ну ёлки-палки :(

Знаю, что некрасиво, но Writer тут не совсем подходит, т.к. уже сформированный список иногда надо модифицировать. Или о чем оно?

P.P.S. попозже посмотрю подробно

Заранее спасибо :)

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Прозревая, что об O(n) и вставке в конце списка. Используй Data.Sequence, например.

Оно ж ленивое, хотя, да, в этом месте State у меня везде вроде Strict. В реальном проекте у меня в таком случае Data.Sequence и используется.

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

Data.Sequence или DList, первое лучше.

Pull-request ушёл, у меня стало лучше в 4 раза, но я не уверен, что я правильно в первый раз померял.

Причина использование ++[a] создает список получение данных из которого будет O(N^2), что совсем не круто.

qnikst ★★★★★
()
Ответ на: комментарий от yoghurt

и второй пул-реквест просто так, в котором не нужны noop, чтобы написать программу :) (а не он в первый добавился)

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от yoghurt

строгое State форсит состояние до WHNF, что в случае списка дает не очень много :)

qnikst ★★★★★
()
Ответ на: комментарий от yoghurt

Но у пацанов же работает! (1, 2)

Ну так суть в том, что портянка невразумительного говна заменяется портянкой невразумительного говна, просто другого. В итоге оказывается, что одно говно говнистее - неизвестно как и почему, потому что копаться в говне никто не станет, control-flow запутан до полного отсутствия понимания как, что и почему выполняется, а потом еще и обрабатывается весьма нетривиальными оптимизациями, которые могут сработать или не сработать из-за малозаметных и маловразумительных различий в этом control-flow.

anonymous
()

По поводу главного вопроса, во первых вопрос с моей стороны? Нафига StateT IO, а не просто State, сейчас эту функциональность ты не используешь совсем и она не должна влиять на производительность. State же уже в CPS стиле, дополнительные фичи ты мог бы получить от Condencity, но у тебя последовательность действий почти нулевая (интерпретация). Много действий в программе, но я чет думаю, что builder (Monoid) и его интерпретатор были бы более чем достаточны, или вообще Free.

qnikst ★★★★★
()
Ответ на: комментарий от anonymous

тоесть я к тому что тебе нужно знать четкую модель логики и скопировать ее,сделав десятки методов под различные комбинации вызовов(у тебя всего десяток операторов,это несколько сотен комбинаций всего)

Ты хотел сказать что нужно просто написать Final Tagless Interpreter? Ну в принципе он тут не сложно пишется, но тогда вопрос заданный в треде пройдёт совсем мимо.

qnikst ★★★★★
()
Ответ на: комментарий от yoghurt

ну и ещё. Тут есть момент где нужно сделать выбор про трусы и крестики^W^W^W про логическое или более «железное» представление.

Вариант-1. Ты представляешь программу как возможно бесконечное дерево (*):

   *
   |
   * = a
   |
   * IF
   | \ 
   |  a
   *

в этом случае вместо JMP instruction-index у тебя будет JMP new-program. Тогда можно делать через Seq/List как ты делал с мелкими изменениями

(*) насамом деле это дерево будет циклическим графом, но с точки зрения интерпретатора это не важно.

Вариант 2. Представлять инструкции, как фиксированный набор команд, в этом случае текущая позиция будет частью стейта, а сам тип для хранения инструкций нужно взять какой-нибудь иммутабельный и эффективный, например Vector, в этом случае состояние процессора это data PState = PState !Int !Registers

Первый подход ближе духу ФП, второй - реальному железу. Какой лучше (не считая того, что они преобразуются один в другой) я ответить не могу за неимением опыта, но подозреваю, что зависит от задачи, которую ты перед собой ставишь.

Ну и как сказал анонимус, тут вполне можно замутить Final Taggedless, и тем самым убрать интерпретацию из рантайма совсем при этом не ломая синтаксис программы. При этом можно будет делать разные интерпретации по одному и тому же dsl, просто указывая тип. Но это вроде выходит за обсуждаемые рамки.

Ещё интересное и выходящее за рамки это то, что у тебя сейчас есть op a () для унарных, тут можно сделать удобнее введя более сложные типы данных, которые не позволят генериьт кривые инструкци, ещё часть всего можно вынести в typelevel, что тоже интересно и позволит, например, создавать разные типы процов и проверять на каком проце какая программа будет работать и не давать писать некорректные. Опять же это выходит далеко за рамки поднятого вопроса (ну и уже написано).

qnikst ★★★★★
()
Ответ на: комментарий от anonymous

Выкинь монады и все будет летать (да и код станет значительно проще и короче).

В общем, так и получилось. По сравнению с лучшим до этого результатом - 0.33 для ручного анроллинга, с выкидыванием State целиком и переносом всего в IO получил 0.24с.

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от qnikst

Нафига StateT IO, а не просто State, сейчас эту функциональность ты не используешь совсем и она не должна влиять на производительность

Есть же PRN :) А так, это все лишь минимальная модель, необходимая для воспроизведения боевых условий, очищенная от мишуры. Настоящий проект на всеобщее обозрение выкачу чуть позже :)

но я чет думаю, что builder (Monoid) и его интерпретатор были бы более чем достаточны, или вообще Free.

Ох, боюсь, это пока за гранью моего понимания.

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от qnikst

Final Tagless Interpreter

Нагуглил, полистал. Блин, это ведь примерно то, что вертелось на уме когда-то, но я подумал, что такое - совсем фантастика. А тут, вот, как всегда, Олег уже сделал это. :) Обязательно ознакомлюсь поближе, спасибо вам на пару с анонимусом.

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от qnikst

в этом случае вместо JMP instruction-index у тебя будет JMP new-program

Об этом тоже думал, но я так и не придумал, как организовать такую бесконечную цикличную структуру. Хотя, ленивый-то язык как-бы сам к такому располагает. Видимо, недостаточно думал)

Вариант 2. Представлять инструкции, как фиксированный набор команд, в этом случае текущая позиция будет частью стейта, а сам тип для хранения инструкций нужно взять какой-нибудь иммутабельный и эффективный, например Vector, в этом случае состояние процессора это data PState = PState !Int !Registers

Практика показала (а я потестил уже массу подходов), что простой проход по списку - вещь самая простая и быстрая (а для меня скорость - в самом приоритете).

ещё часть всего можно вынести в typelevel

Дааа, это отдельная тема и отдельный простор для творчества, но, да, уже далеко за рамками исходной задачи :)

yoghurt ★★★★★
() автор топика

В общем выводы сделаны, направления определены, распоследние результаты на гитхаб запушены, бранча nomonad всех рвёт так, что даже не смешно. Всем спасибо :)

yoghurt ★★★★★
() автор топика

Месье один из самых извращенных хаскеляторов лора. Я потерял сознание примерно на середине этой простыни. upd причем я ее проматывал, а не читал.

Virtuos86 ★★★★★
()
Последнее исправление: Virtuos86 (всего исправлений: 1)
Ответ на: комментарий от yoghurt

Практика показала (а я потестил уже массу подходов), что простой проход по списку - вещь самая простая и быстрая (а для меня скорость - в самом приоритете).

Не может такого быть, что проход по списку будет выполняться быстрее, чем доступ к нужному элементу массива

anonymous
()

Типичная школьная задача (причем школьник на галимом паскале сделал бы куда более быстрое решение), а сколько базвордов и пафоса, ну хотя бы уже не факториал.

anonymous
()
Ответ на: комментарий от anonymous

Seq в компиляторе при формировании кода, в выполнении код - просто список. Массив там не нужен

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Да не будут они работать быстрее (в Хаскеле), пройденный это этап. Т.е. сами джампы-то может и ускорятся, а вот итерация по контейнеру последовательно - замедлится.

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

Talk is cheap, show me the code! А по скорости выполнения будет скорее всего быстрее, да

Не паскаль, но.

~$ cat test.cpp
#include <iostream>
using namespace std;

double R1, R2, R3, R4, R5, R6;
int cmp, pos;

typedef void (*op)(void);

template<int     V, double& R>  void ADD()   { R += V; }
template<double& V, double& R>  void ADD()   { R += V; }
template<double& V, double& R>  void DIV()   { R /= V; }
template<int     V, double& R>  void DIV()   { R /= V; }
template<double& V, double& R>  void EQUAL() { cmp = R == V; }
template<int N>                 void JMP()   { pos = N - 1; }
template<int N>                 void JMT()   { if( cmp ) pos = N - 1; }
template<int     V, double& R>  void MOV()   { R = V; }
template<double& V, double& R>  void MOV()   { R = V; }
template<int     V, double& R>  void MUL()   { R *= V; }
template<double& R>             void PRN()   { cout << R << '\n'; }

void run( op* prog, int n ) {
    for( pos = -1; ++pos < n; )
        prog[ pos ]();
}

int main() {
    op heron[] = {
        MOV<1, R5>,
        MOV<0, R3>,
    // iterStart
        MOV<R3, R4>,
        EQUAL<R2, R4>,
        JMT</*loopEnd*/ 12>,
        MOV<R1, R6>,
        DIV<R5, R6>,
        ADD<R5, R6>,
        DIV<2, R6>,
        MOV<R6, R5>,
        ADD<1, R3>,
        JMP</*iterStart*/ 2>,
    // loopEnd
        PRN<R6>
    };
    
    while( cin >> R1 ) {
       R2 = 20;
       run( heron, sizeof(heron) / sizeof(op) );
    }
}
~$ g++ -Ofast -mtune=native ./test.cpp -o ./Asm
~$ ./measure.sh 
0.03
~$ ./measure.sh 
0.03
~$ ./measure.sh 
0.03
~$ ghc --make -O3 Asm.hs
Linking Asm ...
~$ ./measure.sh 
0.24
~$ ./measure.sh 
0.24
~$ ./measure.sh 
0.26
~$

Можно было еще сделать именованные метки, которые перед запуском бы превращались тупо в позицию (т.е. в то, что сейчас), но раз ты это не сделал, то и я решил не заморачиваться.

anonymous
()
Ответ на: комментарий от anonymous

Ну я и с хаскелем не знаком, но это чо аналог while( cin >> R1 )? o_O

  input <- {-# SCC "READ" #-} liftM (map (read . B.unpack) . B.words) $ B.hGetContents stdin
  forM_ input $ \i -> {-# SCC "ITER" #-} execute (initialRs {r1 = i, r2 = 20 }) h 
anonymous
()
Ответ на: комментарий от anonymous

это чо аналог while( cin >> R1 )? o_O

Почти, в моем примере чтение эффективное (не ленивное).

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от anonymous

А код на плюсах порадовал, спасибо :)

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Ну это какбе чит, сделай нормальные метки. Без меток, понятно, можно просто налепить с-щих ф-й и их вызывать, но это не позволяет работать с кодом как first-class value.

anonymous
()
Ответ на: комментарий от yoghurt

а вот итерация по контейнеру последовательно - замедлится.

Странный ваш хачкель. Но это в общем не суть, просто оно еще будет работать на игрушечных примерах с 10 инструкциями, а представь теперь что у тебя этих инструкций например 10000 и на каждый джамп придется бегать по листу с самого его начала.

anonymous
()
Ответ на: комментарий от anonymous

Ну это какбе чит, сделай нормальные метки. Без меток, понятно, можно просто налепить с-щих ф-й и их вызывать

Внезапно, это можно делать и с метками, перед run делаешь проход по программе и тупо забиваешь нужные функции по смещениям. Это потребует изначально хранить чуть больше информации, но после «препроцессора» будет ровно тот же массив, что и сейчас.

anonymous
()
Ответ на: комментарий от anonymous

Внезапно, это можно делать и с метками, перед run делаешь проход по программе и тупо забиваешь нужные функции по смещениям.

Ты, кстати, подал мне идею, но что-то мне подсказывает, что вызов функции по указателю тяжелее ручного свича и вызова _известной_ функций

yoghurt ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

но что-то мне подсказывает, что вызов функции по указателю тяжелее ручного свича и вызова _известной_ функций

Да тяжелее, в полтора раза где-то. Вот только твой switch + передача параметров для _известной_ функций будут гораздо тяжелее.

anonymous
()
Ответ на: комментарий от anonymous

Да тяжелее, в полтора раза где-то

Имелось ввиду вызов _известной_ функций против вызова по указателю.

anonymous
()
Ответ на: комментарий от yoghurt

Ты, кстати, подал мне идею, но что-то мне подсказывает, что вызов функции по указателю тяжелее ручного свича и вызова _известной_ функций

Только в том случае, если branch predictor будет срабатывать. В рассматриваемом случае он не будет, так что косвенный вызов заведомо дешевле.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.