Pattern-matching FTW!

Submitted by 0xd34df00d on Wed, 11/30/2011 - 13:11

Все никак не привыкну мыслить целиком в функциональном стиле, с паттерн-матчингами и прочими ништяками.

В одном моем проектике (который эти самые пресловутые ГА) нужно было представлять математические выражения деревьями и всю работу с этими выражениями делать через эти деревья. Соответственно, был статичный словарь ([(a, b)]) идентификаторов функций и, собственно, самих функций. Вернее, два таких словаря, для унарных и бинарных функций. Что-то типа такого:

  1. data UnaryFunc = Sin | Cos | Log | Tan | Asin | Acos | Atan
  2. deriving (Show, Eq, Ord)
  3.  
  4. data BinaryFunc = Plus | Minus | Mul | Div | Pow
  5. deriving (Show, Eq, Ord)
  6.  
  7. unaryOps :: Floating a => [(UnaryFunc, a -> a)]
  8. unaryOps = [ (Sin, sin), (Cos, cos), (Log, log), (Tan, tan), (Asin, asin), (Acos, acos), (Atan, atan) ]
  9.  
  10. binaryOps :: Floating a => [(BinaryFunc, a -> a -> a)]
  11. binaryOps = [ (Plus, (+)), (Minus, (-)), (Mul, (*)), (Div, (/)), (Pow, (**)) ]

Соответственно, кусок функции вычисления значения дерева в точке, обрабатывающий вершины с функциями, выглядел как-то так:

  1. evalTree vars (NUn f t) | Just f' <- lookup f unaryOps = f' $ evalTree vars t
  2. | otherwise = error $ "Unknown uf " ++ show f
  3. evalTree vars (NBin f l r) | Just f' <- lookup f binaryOps = f' (evalTree vars l) (evalTree vars r)
  4. | otherwise = error $ "Unknown bf " ++ show f

Страшно и тупо, правда? Страшнее становится, если запустить программу под профилером и посмотреть, что жрет больше всего процессора и больше всего грузит GC. Победителями оказываются, собственно:

COST CENTRE                    MODULE               %time %alloc
evalTree                       ExprTree              47.7   13.6                                                                                                                                                                                                                                                         
binaryOps                      Funcs                 17.2   47.9

Это собственные значения, без учета вызываемых в случае evalTree подфункций. И тут даже и близко нет ни операций над матрицами, ни вычислений якобиана, ни символьного дифференцирования. Ужас-ужас, короче.

Но тут стоит посмотреть, что список функций-то статичный, и, по факту, стоит вспомнить про паттерн-матчинг. Например, binaryFuncs перепишется следующим образом:

  1. binaryOps :: Floating a => BinaryFunc -> a -> a -> a
  2. binaryOps Plus = (+)
  3. binaryOps Minus = (-)
  4. binaryOps Mul = (*)
  5. binaryOps Div = (/)
  6. binaryOps Pow = (**)

Функция, вычисляющая значение дерева, в свою очередь будет выглядеть так:

  1. evalTree :: Floating a => [(String, a)] -> ExprTree a -> a
  2. evalTree _ (LC c) = c
  3. evalTree vars (LVar (Var v)) | Just c <- lookup v vars = c
  4. | otherwise = error $ "Unknown var " ++ v
  5. evalTree vars (NUn f t) = unaryOps f $ evalTree vars t
  6. evalTree vars (NBin f l r) = binaryOps f (evalTree vars l) (evalTree vars r)

Одна эта мелкая оптимизация ускоряет выполнение раза в два-три и обеспечивает существенно меньшее GC time, что существенно, так как алгоритм отлично параллелится, и
убивать возможность распараллеливания из-за GC не очень бы хотелось.

В результате, правда, binaryOps все еще жрет 20% процессора и ответственна за треть аллокаций. Однако, стоит иметь ввиду такой очевидный факт, что компиляция с -prof -auto-all существенно меняет то, как исполняется код, и отменяет часть оптимизаций, поэтому ориентироваться на конкретные цифры бессмысленно. Главное — что скорость без профилирования существенно повыше стала.

В принципе, если бы не нужно было бы Ord и Eq от деревьев, то можно было бы хранить сами функции в вершинах вместо их идентификаторов.

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

Правда, для этого придется еще пообмазываться type families всякими, ибо хранить уже скомпилированное дерево в том же ExprTree не получится, а генетические алгоритмы у меня работают не напрямую с деревьями, а с инстансами тайпкласса GAble a, поэтому нужно будет запилить что-то типа

  1. class (...) => GAble a where
  2. type Compiled a :: * -> *