Давайте рассмотрим несколько вещей.
Бенчмаркинг
Первое: давайте удостоверимся, что мы на самом деле делаем улучшение, как мы идем!Для этого нам понадобятся некоторые ориентиры. Пакет criterion идеально подходит для этого. Мы также обязательно скомпилируем с оптимизацией (так -O2
по всем вызовам GHC). Вот как простая настройка вверх тест может быть:
import Criterion.Main
-- your code goes here
main = defaultMain
[ bench "findNums 100 2" (nf (uncurry findNums) (100, 2))
, bench "findNums 800 2" (nf (uncurry findNums) (800, 2))
]
Можно также осуществить тест, как nf (findNums 100) 2
, но я выбираю этот путь, так что мы не можем «обмануть», предварительно рассчитав таблицу поиска для 100
, тем самым толкая вся работа в контрольной установке, а не в той части, где фактически выполняется эталон. Вот результат для первоначальной реализации:
benchmarking 100 2
time 762.7 ns (757.4 ns .. 768.5 ns)
1.000 R² (1.000 R² .. 1.000 R²)
mean 762.5 ns (760.4 ns .. 765.3 ns)
std dev 7.706 ns (6.378 ns .. 10.59 ns)
benchmarking 800 2
time 29.17 s (28.28 s .. 29.87 s)
1.000 R² (1.000 R² .. 1.000 R²)
mean 29.26 s (29.08 s .. 29.35 s)
std dev 159.2 ms (0.0 s .. 165.2 ms)
variance introduced by outliers: 19% (moderately inflated)
Используйте библиотеки
Теперь, низко висящие плоды использовать существующие реализации вещей и надеются, что их авторы сделали что-то лучше, чем у нас. С этой целью мы будем использовать стандартную функцию (^)
вместо pow
и integerRoot
от arithmoi вместо root
. Кроме того, мы заменим ленивый foldr
на строгий номер foldl
. Для моего собственного здравомыслия я также переформатировал очень длинную линию на короткие. Полный результат теперь выглядит следующим образом:
import Criterion.Main
import Data.List
import Math.NumberTheory.Powers
sum' :: Num a => [a] -> a
sum' = foldl' (+) 0
findNums :: Int -> Int -> Int
findNums a b = length
[ xs
| xs <- drop 1 . subsequences $ [x^b | x <- [1..c]]
, sum' xs == a
] where c = integerRoot b a
main = defaultMain
[ bench "100 2" (nf (uncurry findNums) (100, 2))
, bench "800 2" (nf (uncurry findNums) (800, 2))
]
Результаты тестирования выглядят, как это сейчас:
benchmarking 100 2
time 722.8 ns (721.3 ns .. 724.3 ns)
1.000 R² (1.000 R² .. 1.000 R²)
mean 722.6 ns (721.4 ns .. 724.1 ns)
std dev 4.440 ns (3.738 ns .. 5.674 ns)
benchmarking 800 2
time 17.16 s (16.93 s .. 17.64 s)
1.000 R² (1.000 R² .. 1.000 R²)
mean 17.05 s (16.99 s .. 17.15 s)
std dev 88.10 ms (0.0 s .. 94.58 ms)
Чуть менее в два раза быстрее с очень небольшим усилием. Ницца!
Лучше алгоритм
Существенная проблема с subsequences
является то, что, даже если мы вычисляем, что sum' [x,y,z] > a
, мы по-прежнему смотреть на все более длинные подпоследовательности, которые начинаются с [x,y,z]
. Учитывая структуру возвращаемого типа 44843689053679157146564308888, мы мало что можем с этим поделать; поэтому давайте разработаем реализацию, которая дает нам немного больше структуры. Мы построим дерево, где пути от корня до любого внутреннего узла дают нам подпоследовательность.
import Data.Tree
subsequences :: [a] -> Forest a
subsequences [] = []
subsequences (x:xs) = Node x rest : rest where
rest = subsequences xs
(Просто для удовольствия, это приводит к экспоненциально большие семантические деревья с очень малым использования пространства - примерно столько же пространства, как в первоначальном списке -. Из-за агрессивного обмена поддерева) Что здорово об этом представлении, если мы прекратите поиск, мы отсекаем огромные полосы неинтересных результатов. Это может быть реализовано путем реализации что-то вроде takeWhile
для списков:
takeWhileTree :: Monoid m => (m -> Bool) -> Forest m -> Forest m
takeWhileTree predicate = goForest mempty where
goForest m forest = forest >>= goTree m
goTree m (Node m' children) =
[Node m (goForest (m <> m') children) | predicate m']
Давайте дадим ему попробовать. Полный код теперь:
import Criterion.Main
import Data.Foldable
import Data.Monoid
import Data.Tree
import Math.NumberTheory.Powers
subsequencesTree :: [a] -> Forest a
subsequencesTree [] = []
subsequencesTree (x:xs) = Node x rest : rest where
rest = subsequencesTree xs
takeWhileTree :: Monoid m => (m -> Bool) -> Forest m -> Forest m
takeWhileTree predicate = goForest mempty where
goForest m forest = forest >>= goTree m
goTree m (Node m' children) = let m'' = m <> m' in
[Node m' (goForest m'' children) | predicate m'']
leaves :: Forest a -> [[a]]
leaves [] = [[]]
leaves forest = do
Node x children <- forest
xs <- leaves children
return (x:xs)
findNums :: Int -> Int -> Int
findNums a b = length
[ xs
| xs <- leaves
. takeWhileTree (<= Sum a)
. subsequencesTree
$ [Sum (x^b) | x <- [1..c]]
, fold xs == Sum a
] where c = integerRoot b a
main = defaultMain
[ bench "100 2" (nf (uncurry findNums) (100, 2))
, bench "800 2" (nf (uncurry findNums) (800, 2))
]
Это выглядит как много работы, но с таймингами, это действительно окупается:
benchmarking 100 2
time 16.67 μs (16.53 μs .. 16.77 μs)
0.999 R² (0.999 R² .. 1.000 R²)
mean 16.60 μs (16.52 μs .. 16.72 μs)
std dev 325.4 ns (270.5 ns .. 444.1 ns)
variance introduced by outliers: 17% (moderately inflated)
benchmarking 800 2
time 22.59 ms (22.26 ms .. 22.89 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 22.44 ms (22.34 ms .. 22.57 ms)
std dev 260.3 μs (191.6 μs .. 332.2 μs)
Это фактор ускорения около 1000 на findNums 800 2
.
распараллеливания
У меня был пойти на распараллеливание это с помощью concat
и parMap
в takeWhileTree
вместо (>>=)
, так что отдельные ветви дерева будут изучены параллельно.В каждом случае накладные расходы на распараллеливание намного перевешивают преимущество нескольких потоков. Хорошо, что мы поставили этот бенчмарк в начале!
О, мой господин. Я не знаю, с чего начать понимать, что происходит. Я понимаю цель бенчмаркинга. Я понимаю, что вы создаете «Лес» с подпоследовательностями. Сначала я начну с этой функции. В этой части 'sequenceencesTree (x: xs) = Node x rest: rest where rest = sequenceencesTree xs', я вижу, как вы создаете узел, но я не уверен, как вы размещаете его в лесу. Есть ли подробное объяснение этого поведения? – terminix00
@ user2977382 Ну, 'type Forest a = [Tree a]', правильно? Таким образом, лес - это всего лишь список деревьев. И подпоследовательности 'xs' либо включают первый элемент' xs', либо нет. Итак, 'Node x rest' - это дерево, где все пути начинаются с элемента' x', а 'rest' - это лес, в котором каждое дерево имеет пути, которые не начинаются с' x'. И «Node x rest: rest» - это лес, который включает в себя оба - поэтому есть пути, которые включают «x» и пути, которые этого не делают. –
Хорошо, это дерево создает все возможные последовательности? И вы просеиваете соответствующие деревья с помощью takeWhileTree, основываясь на предикате суммы дерева, меньшего чем? – terminix00