Во-первых, еще одна точка данных: структура данных Set
в модуле Data.Set
является двоичным деревом. Я перевел вашу fillTree
функцию, чтобы использовать ее вместо:
import qualified Data.Set as Set
import Data.Set (Set)
fillSet :: Int -> Set Int -> Set Int
fillSet 10000 set = set
fillSet x set = let a = Set.insert x set
in fillSet (x + 1) a
Выполнение fillSet 1 Set.empty
в GHCi, включая немного дополнительных вычислений, чтобы убедиться, что весь результат оценивается, выполняется без заметной задержки. Таким образом, это, похоже, указывает на то, что проблема заключается в вашей реализации.
Для начала, я подозреваю, что самая большая разница между использованием Data.Set.Set
и вашей реализацией заключается в том, что если я правильно читаю ваш код, вы на самом деле не тестируете двоичное дерево. Вы тестируете слишком сложный связанный список - т.е. максимально несбалансированное дерево - в результате вставки элементов в возрастающем порядке. Data.Set.Set
использует сбалансированное двоичное дерево, которое лучше обрабатывает патологические данные в этом случае.
Мы также можем посмотреть на определение Set
:
data Set a = Tip
| Bin {-# UNPACK #-} !Size a !(Set a) !(Set a)
Не вдаваясь в подробности, это говорит о том, что отслеживает размер дерева и избегает нескольких менее полезных слоев косвенности, которые в противном случае существовали бы в типе данных.
Полный источник модуля Data.Set
можно найти здесь ; вам может показаться полезным учиться.
Еще несколько наблюдений, чтобы продемонстрировать разницу между разными способами его запуска. Я добавил следующее в ваш код:
toList EmptyTree = []
toList (Node x l r) = toList l ++ [x] ++ toList r
main = print . sum . toList $ fillTree 1 EmptyTree
Это обходит дерево, суммирует элементы и печатает итог, что должно гарантировать, что все форсировано. Моя система, вероятно, несколько необычна, поэтому вы можете получить довольно разные результаты, попробовав это самостоятельно, но относительные различия должны быть достаточно точными Некоторые результаты:
Использование runhaskell
, что примерно эквивалентно запуску его в GHCi:
real 1m36.055s
user 0m0.093s
sys 0m0.062s
Здание с ghc --make -O0
:
real 0m3.904s
user 0m0.030s
sys 0m0.031s
Здание с ghc --make -O2
:
real 0m1.765s
user 0m0.015s
sys 0m0.030s
Вместо этого используется моя эквивалентная функция, основанная на Data.Set
:
Использование runhaskell
:
real 0m0.521s
user 0m0.031s
sys 0m0.015s
Использование ghc --make -O2
:
real 0m0.183s
user 0m0.015s
sys 0m0.031s
И мораль сегодняшней истории такова: Оценка выражений в GHCi и синхронизация их с помощью секундомера - очень и очень плохой способ проверить производительность вашего кода.