Ниже приведены 3 функции, которые находят последний, но второй элемент в списке. Тот, кто использует, last . init
кажется намного быстрее, чем остальные. Я не могу понять, почему.
Для тестирования я использовал входной список [1..100000000]
(100 миллионов). Последний запускается почти мгновенно, тогда как остальные занимают несколько секунд.
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
init
был оптимизирован, чтобы избежать «распаковки» списка несколько раз.myButLast
намного медленнее? Кажется, что это не распаковка какого-либо списка, а просто обход его какinit
функция ...[x, y]
является аббревиатурой(x:(y:[]))
, поэтому он распаковывает внешние недостатки, вторые минусы, и проверяет , если хвост второгоcons
есть[]
. Кроме того, второе предложение снова распакует список в(x:xs)
. Да, распаковка достаточно эффективна, но, конечно, если это происходит очень часто, это замедлит процесс.init
не повторяет, является ли ее аргумент одноэлементным списком или пустым списком. Как только начинается рекурсия, она просто предполагает, что первый элемент будет привязан к результату рекурсивного вызова.myButLast
автоматически дает вам оптимизированную версию . Я думаю, что скорее ускорение списка, который виноват в ускорении.Ответы:
При изучении скорости и оптимизации очень легко получить дико неверные результаты . В частности, вы не можете сказать, что один вариант быстрее другого, не упоминая версию компилятора и режим оптимизации вашей настройки бенчмаркинга. Даже в этом случае современные процессоры настолько сложны, что в них предусмотрены предикторы ветвления на основе нейронной сети, не говоря уже о всех видах кэшей, поэтому даже при тщательной настройке результаты сравнительного анализа будут размытыми.
Что, как говорится...
Бенчмаркинг - наш друг.
criterion
это пакет, который предоставляет расширенные инструменты для тестирования. Я быстро набросал эталон, как это:Как видите, я добавил вариант, который явно соответствует двум элементам одновременно, но в остальном это тот же самый код. Я также запускаю тесты в обратном порядке, чтобы знать о смещении из-за кеширования. Итак, давайте побежим и посмотрим!
Похоже, наша "медленная" версия совсем не медленная! И тонкости сопоставления с образцом ничего не добавляют. (Небольшое ускорение, которое мы видим между двумя последовательными прогонами,
match2
я приписываю эффектам кэширования.)Есть способ получить больше «научных» данных: мы можем
-ddump-simpl
взглянуть на то, как компилятор видит наш код.Осмотр промежуточных сооружений - наш друг.
«Ядро» является внутренним языком GHC. Каждый исходный файл Haskell упрощается до Core перед преобразованием в окончательный функциональный граф для выполнения системой времени выполнения. Если мы посмотрим на этот промежуточный этап, он скажет нам, что
myButLast
иbutLast2
эквивалентны. Нужно посмотреть, поскольку на этапе переименования все наши красивые идентификаторы случайным образом искажаются.Кажется, что
A1
иA4
являются наиболее похожими. Тщательное обследование покажет , что на самом деле кодовые структуры вA1
иA4
идентичны. ЭтоA2
иA3
они одинаковы, также разумно, так как оба определены как состав из двух функций.Если вы собираетесь тщательно изучить
core
вывод, имеет смысл также указывать такие флаги , как-dsuppress-module-prefixes
и-dsuppress-uniques
. Они делают его намного проще для чтения.Короткий список наших врагов тоже.
Итак, что может пойти не так с бенчмаркингом и оптимизацией?
ghci
Будучи разработанным для интерактивного воспроизведения и быстрой итерации, компилирует исходный код Haskell для определенного вида байтового кода, а не для конечного исполняемого файла, и избегает дорогих оптимизаций в пользу более быстрой перезагрузки.Это может выглядеть грустно. Но это действительно не то, что должно волновать программиста на Haskell, большую часть времени. Реальная история: у меня есть друг, который только недавно начал изучать Haskell. Они написали программу для численного интегрирования, и она была медлительной. Поэтому мы сели вместе и написали категорическое описание алгоритма с диаграммами и прочим. Когда они переписали код, чтобы привести его в соответствие с абстрактным описанием, он волшебным образом стал быстрым, как и гепард, и также сократил объем памяти. Мы рассчитали π в кратчайшие сроки. Мораль истории? Идеальная абстрактная структура, и ваш код будет оптимизирован.
источник
ghci
кажется, что дает разные результаты (с точки зрения скорости) по сравнению с созданием exe первым, как вы сказали.