Пожалуйста, сообщите нам о других «полезных подсказках», которые стоит добавить сюда!
11.1. Быстрее: как быстрее создавать программы
-
Don’t use -O or (especially) -O2: -
Используя их, вы сообщаете GHC, что готовы мириться с более длительной компиляцией ради повышения качества кода.
GHC удивительно быстр при обычной компиляции без
-O! - Используйте больше памяти:
-
В разумных пределах увеличение объёма памяти для кучи уменьшает количество сборок мусора в GHC, а значит, сокращает время компиляции. Если вы используете параметр
-Rghc-timing, вы получите отчёт сборщика мусора. (Кроме того, можно использовать дешёвый и грубый параметр+RTS -S -RTS, чтобы выводить статистику GC прямо в стандартный поток ошибок.)Если на сборку мусора уходит более 20% общего времени, возможно, поможет увеличение объёма памяти: используйте параметр
-H⟨size⟩(см.-H [⟨size⟩]). Также может помочь увеличение размера области выделения памяти по умолчанию, используемой RTS компилятора: используйте параметр+RTS -A⟨size⟩ -RTS(см.-A ⟨size⟩).Если GHC продолжает плохо распоряжаться памятью, пожалуйста, сообщите об этом как об ошибке.
- Не используйте слишком много памяти!
-
Как только GHC и его «соседи» (другие процессы на компьютере) начинают использовать больше памяти, чем доступно в физической памяти компьютера, и система начинает «буксовать», всё пропало. Время компиляции станет не просто ужасным, а катастрофическим! Используйте, например, встроенную команду csh time, чтобы узнать количество ошибок страниц.
Если вы не знаете, что такое виртуальная память, свопинг и ошибки страниц, или не знаете конфигурацию памяти своего компьютера, не пытайтесь хитрить с использованием памяти: вы только усложните себе жизнь (и, вероятно, другим тоже).
- При компоновке старайтесь использовать локальные диски:
-
Объектные файлы и библиотеки Haskell, как правило, велики, поэтому передача данных на удалённую файловую систему и обратно может занимать много секунд.
Вполне разумно компилировать на быстрой машине, используя удалённо подключённые диски, а затем выполнять компоновку на медленной машине, к которой ваши диски подключены напрямую.
-
Don’t derive/use Read unnecessarily: -
Это некрасиво и медленно.
- Некоторые конструкции программ GHC компилирует медленно:
-
Мы предпочли бы, чтобы вы сообщили о таком поведении как об ошибке, чтобы мы могли попытаться её исправить.
Чтобы определить, какая часть компилятора ведёт себя неправильно, используйте параметр
-v2.
11.2. Быстрее: как создавать программы, которые работают быстрее
Главный инструмент для ускорения программ на Haskell — средства профилирования GHC, описание которых приведено отдельно в разделе Профилирование. Ничто не заменит выяснение того, на что действительно тратятся время и память вашей программы, в отличие от ваших предположений.
И ещё один важный момент: безусловно, лучший способ значительно повысить производительность программы — использовать более эффективные алгоритмы. После того как профилирование выявит главных виновников расхода времени, возможно, лучше будет переосмыслить программу, а не пробовать все перечисленные ниже приёмы.
Ещё один чрезвычайно эффективный способ ускорить программу — использовать библиотечный код, который Кто-то Другой Тщательно Оптимизировал. Возможно, вы сможете написать более быструю сортировку quicksort, чем та, что находится в Data.List, но на это уйдёт гораздо больше времени, чем на ввод import Data.List.
Пожалуйста, сообщайте о программах, скомпилированных GHC и работающих слишком медленно. Поскольку в наши дни у GHC нет серьёзных конкурентов в области производительности, трудно сказать, что именно считать слишком медленной работой, поэтому полагайтесь на собственное мнение! Разумеется, если программа, скомпилированная GHC, работает медленнее той же программы, скомпилированной NHC или Hugs, это определённо ошибка.
-
Optimise, using -O or -O2: -
Это самый простой способ ускорить программу. Время компиляции увеличится, особенно с
-O2.В настоящее время
-O2почти не отличается от-O. - Компилируйте с помощью LLVM:
-
Генератор кода LLVM иногда справляется с созданием быстрого кода гораздо лучше, чем генератор машинного кода. Это не всегда так и зависит от кода. Наибольший прирост производительности при компиляции через LLVM, по-видимому, наблюдается у программ с большим количеством числовых вычислений. Также можно экспериментировать с передачей LLVM конкретных флагов с помощью флагов
-optlo ⟨option⟩и-optlc ⟨option⟩. Будьте осторожны: установка этих флагов не позволяет GHC задавать обычные флаги для оптимизатора и компилятора LLVM. - Перегруженные функции — не ваши друзья:
-
Перегрузка в Haskell (с помощью классов типов) элегантна, изящна и так далее, но если оставить её во внутреннем цикле, она губительно сказывается на производительности. Как от неё избавиться?
- Указывайте явные сигнатуры типов:
-
Сигнатуры — это основной приём; указывать их для экспортируемых функций верхнего уровня — в любом случае хорошая практика разработки ПО. (Совет: параметр
-Wmissing-signaturesпоможет следовать этой практике.)Автоматическая специализация перегруженных функций (с помощью
-O) должна обрабатывать перегруженные локальные и/или неэкспортируемые функции. -
Use SPECIALIZE pragmas: -
Специализируйте перегрузку ключевых функций в своей программе. См. прагму SPECIALIZE и прагму SPECIALIZE для экземпляров.
- «Но как узнать, где появляется перегрузка?»
-
Простой способ: выполните grep (поиск) в файлах интерфейса по сигнатурам перегруженных типов. Просмотреть файлы интерфейса можно с помощью параметра
--show-iface ⟨file⟩(см. Другие параметры, связанные с файлами интерфейса).$ ghc --show-iface Foo.hi | grep -E '^[a-z].*::.*=>'
- Строгие функции — ваши лучшие друзья:
-
А ленивое сопоставление с образцом, помимо прочего, — ваш враг.
(Если вы не знаете, что такое «строгая функция», обратитесь к учебнику по функциональному программированию. Пара предложений с объяснением здесь, скорее всего, мало чем поможет.)
Рассмотрим следующие два фрагмента кода:
f (Wibble x y) = ... # strict f arg = let { (Wibble x y) = arg } in ... # lazyПервый вариант даст гораздо более эффективный код.
Менее искусственный пример показывает использование
BangPatternsвletsдля получения более строгого кода (что полезно):f (Wibble x y) = let !(a1, b1, c1) = unpackFoo x !(a2, b2, c2) = unpackFoo y in ... - GHC любит типы данных с одним конструктором:
-
Ещё лучше, если функция строга по типу с одним конструктором (то есть типу, имеющему только один конструктор данных; например, кортежи — это типы с одним конструктором).
- Newtype лучше, чем datatype:
-
Если у вашего типа данных один конструктор с одним полем, используйте объявление
newtypeвместо объявленияdata. В большинстве случаевnewtypeбудет оптимизирован. - «Как узнать строгость функции?»
-
Не гадайте — проверьте.
Найдите функцию в файле интерфейса, затем найдите третье поле в прагме; там должно быть указано
Strictness: ⟨string⟩. Значение ⟨string⟩ описывает строгость аргументов функции: описание нотации строгости см. в комментариях к GHC.Для «распаковываемого» аргумента
U(...)содержащаяся там информация описывает строгость его компонентов. Например, если аргумент — пара и указаноU(AU(LSS)), это означает: «первый компонент пары не используется; второй компонент сам можно распаковать, и он состоит из трёх компонентов (первый ленивый, второй и третий строгие)».Если функция не экспортируется, скомпилируйте код с дополнительным флагом
-ddump-simpl; рядом с сигнатурой каждой связки будет выведена та же прагматическая информация, которая помещается в файл интерфейса. (К тому же на синтаксис Core приятно смотреть!) -
Force key functions to be INLINEd (esp. monads): -
Добавление прагм
INLINEк некоторым часто используемым функциям может существенно повлиять на производительность. См. прагму INLINE. -
Explicit export list: -
Если в модуле нет явного списка экспорта, GHC вынужден считать, что экспортируется всё содержимое этого модуля. Это может отрицательно сказаться на оптимизации. Например, если часть кода фактически не используется (возможно, из-за эффектов встраивания), GHC не сможет её удалить, потому что она экспортируется и другой модуль может рассчитывать на её наличие.
Если GHC знает, что фрагменты кода не экспортируются, он может оптимизировать их гораздо агрессивнее.
- Изучите синтаксис Core!
-
(Форма, в которой GHC преобразует ваш код.) Просто запустите компиляцию с параметром
-ddump-simpl(не забудьте про-O).Если профилирование указало на конкретные функции, изучите их код Core.
lets— плохо,cases— хорошо, словари (d.⟨Class⟩.⟨Unique⟩) [и всё, связанное с перегрузкой] — плохо, вложенные лямбды — плохо, явные конструкторы данных — хорошо, примитивные операции (например,==#) — хорошо, … - Используйте аннотации строгости:
-
Аннотация строгости (
!) для поля конструктора полезна по двум причинам: она добавляет строгость в программу, предоставляя анализатору строгости больше информации, и может помочь уменьшить утечки памяти.Она может быть полезна и в третьем отношении: при использовании вместе с
-funbox-strict-fields(см. платформонезависимые флаги -f*) строгое поле можно распаковать или представить без указателя в конструкторе, устранив один или несколько уровней косвенности. Распаковка выполняется только для типов данных с одним конструктором (например, хорошим кандидатом являетсяInt).Использовать
-funbox-strict-fieldsимеет смысл только вместе с-O, поскольку иначе дополнительные упаковка и распаковка не будут оптимизированы. Более того, возможно, что-funbox-strict-fieldsухудшит производительность даже при использовании-O, хотя это маловероятно (сообщите нам, если такое случится). - Используйте типы без упаковки (расширение GHC):
-
Когда вам действительно нужна скорость и вы хотите работать непосредственно с «сырыми данными». Информацию об использовании типов без упаковки см. в разделе Типы без упаковки.
Прежде чем прибегать к явным типам без упаковки, сначала попробуйте использовать строгие поля конструкторов и
-funbox-strict-fields(см. выше). Так ваш код останется переносимым. -
Use foreign import (a GHC extension) to plug into fast libraries: -
Это потребует реальных усилий, но… Существует множество тщательно оптимизированных библиотек, и лучше не пытаться с ними конкурировать, а связать программу с ними.
В разделе Интерфейс внешних функций (FFI) описан интерфейс внешних функций.
-
Use unboxed arrays (UArray) -
GHC поддерживает массивы с элементами без упаковки для нескольких основных арифметических типов элементов, включая
IntиChar: подробности см. в библиотеке Data.Array.Unboxed. Такие массивы, вероятно, будут намного быстрее стандартных массивов Haskell 98 из библиотеки Data.Array. - Увеличьте размер кучи!
-
Если статистика GC вашей программы (параметр RTS
-S [⟨file⟩]) показывает, что сборка мусора выполняется часто (скажем, занимает более 20% времени выполнения), увеличение объёма памяти может помочь — воспользуйтесь параметрами RTS-H [⟨size⟩]или-A ⟨size⟩(см. Параметры RTS для управления сборщиком мусора). В качестве общего правила попробуйте задать-H [⟨size⟩]объём памяти, который вы готовы выделить процессу, или передать-H [⟨size⟩]без аргумента, чтобы GHC вычислил значение на основе объёма живых данных. - Компактно храните данные:
-
Модуль GHC.Compact позволяет повысить эффективность сборки мусора для долгоживущих структур данных. При уплотнении структуры данных объекты собираются вместе в памяти и рассматриваются сборщиком мусора как единый объект, а не обходятся по отдельности.
11.3. Меньше: как создавать программы меньшего размера
Уменьшите порог «выполнять любой ценой» для встраивания небольших выражений. В крайних случаях задайте параметр -funfolding-use-threshold=0. («Встраивать следует только выражения с нулевой стоимостью.») Предупреждение: за исключением некоторых специализированных случаев (например, парсеров Happy), это, скорее всего, на деле увеличит размер программы, поскольку встраивание обычно позволяет выполнить дополнительные оптимизирующие преобразования.
Избегайте Prelude.Read.
Используйте strip для исполняемых файлов.
11.4. Экономнее: как создавать программы, потребляющие меньше памяти кучи
«Кажется, у меня утечка памяти…»
Запустите программу ещё раз с параметром +RTS -S, и сомнения исчезнут! (Вы увидите, как использование кучи всё увеличивается и увеличивается…) (Хм… возможно, с параметром RTS -G1 это будет сделать ещё проще; итак… ./a.out +RTS -S -G1)
И снова: средства профилирования (Профилирование) — основной инструмент для того, чтобы разобраться в использовании памяти вашей программой.
Строгие функции полезны для экономии памяти так же, как и для повышения скорости, о чём говорилось в предыдущем разделе. Строгие функции сразу переходят к делу, вместо того чтобы заполнять кучу замыканиями (заметками системы о том, как вычислить что-либо, если это в конечном счёте понадобится).
11.5. Управление встраиванием с помощью флагов оптимизации.
Встраивание — одна из основных оптимизаций, выполняемых GHC. Отчасти это связано с тем, что встраивание часто позволяет запускать другие оптимизации. К сожалению, у этого есть и обратная сторона. Встраивание нередко сокращает накладные расходы во время выполнения, но обычно увеличивает не только размер программы, но и нагрузку на компилятор. В крайних случаях это может сделать компиляцию некоторого кода невозможной.
По этой причине GHC предоставляет различные способы настройки поведения встраивания.
11.5.1. Создание развёрток
Чтобы встроить функцию из другого модуля, GHC требуется её развёртка. Следующие флаги позволяют управлять созданием развёрток, делая его более или менее вероятным:
11.5.2. Решения о встраивании
Если развёртка доступна, следующие флаги могут повлиять на решение GHC о встраивании конкретной связки.
-funfolding-use-threshold=⟨n⟩-funfolding-case-threshold=⟨n⟩-funfolding-case-scaling=⟨n⟩-funfolding-dict-discount=⟨n⟩-funfolding-fun-discount=⟨n⟩
Если упрощатель исчерпывает лимит тактов из-за цикла встраивания, пользователям рекомендуется уменьшить значения -funfolding-case-threshold=⟨n⟩ или -funfolding-case-scaling=⟨n⟩, чтобы ограничить встраивание во вложенные выражения, сохранив при этом возможность увеличить коэффициент тактов.
Значения по умолчанию настроены так, чтобы не вызывать регрессий для большинства пользовательских программ. Значение -funfolding-case-threshold=⟨n⟩ в диапазоне 1–2 в сочетании со значением -funfolding-case-scaling=⟨n⟩ в диапазоне 15–25 обычно приводит к небольшому ухудшению времени выполнения, но предотвращает выход большинства циклов встраивания из-под контроля.
В крайних случаях полезно ещё сильнее уменьшить коэффициент масштабирования и порог, однако тогда весьма вероятно, что полезное встраивание тоже будет заблокировано, что приведёт к значительному ухудшению времени выполнения.
В таких случаях рекомендуется перенести проблемный фрагмент кода в отдельный модуль и изменить параметры встраивания только для проблемного модуля.
11.5.3. Встраивание обобщённых функций
Есть также флаги, предназначенные специально для встраивания обобщённых функций:
11.6. Управление специализацией
GHC умеет оптимизировать полиморфный код для конкретных экземпляров классов типов в месте использования. Это называется специализацией и включается с помощью -fspecialise, включённого по умолчанию при -O1 и выше.
GHC создаёт копию перегруженной функции и оптимизирует её для заданного экземпляра класса типов. После этого вызовы перегруженной функции со статически известным классом типов, для которого создана специализация, заменяются вызовами специализированной версии функции.
Это часто крайне важно для устранения накладных расходов во время выполнения. Однако такая оптимизация может потребовать создания множества копий перегруженных функций, поэтому GHC не всегда применяет её по умолчанию, даже когда это возможно.
Чтобы GHC мог выполнить специализацию, как минимум должны быть известны экземпляр, для которого она выполняется, и развёртка перегруженной функции.
11.6.1. Часто используемые сочетания флагов и прагм
Для приложений, не выполняющих большого объёма вычислений, значений по умолчанию часто достаточно: они обеспечивают разумный баланс между временем компиляции и временем выполнения.
В библиотеках, если экспортируемые функции значительно выиграют от специализации, рекомендуется включить -fexpose-overloaded-unfoldings или вручную добавить прагмы INLINEABLE к функциям, важным для производительности. Это позволит пользователям библиотеки специализировать любые экспортируемые библиотекой перегруженные функции, если это будет полезно.
Если производительность ключевых частей приложения зависит от специализации, использование прагм SPECIALIZE в сочетании с -fexpose-overloaded-unfoldings или INLINEABLE для ключевых перегруженных функций должно позволить специализировать эти функции, не слишком увеличивая общее время компиляции.
Для кода с большим объёмом вычислений, где важно устранить как можно больше накладных расходов, рекомендуется использовать сочетание -fspecialise-aggressively и -fexpose-overloaded-unfoldings или -fexpose-all-unfoldings. Однако это значительно увеличит время компиляции.
11.6.2. Доступность развёрток
Доступность развёрток в первую очередь определяется этими флагами.
Для специализации особенно важны следующие флаги:
Первый делает доступными все развёртки, что может значительно увеличить время компиляции. Второй открывает доступ только к перегруженным функциям. Обычно, если требуется обеспечить специализацию, предпочтительнее использовать -fexpose-overloaded-unfoldings, а не -fexpose-all-unfoldings.
11.6.3. Когда GHC создаёт специализации
Функции рассматриваются для специализации неявно, когда GHC обнаруживает использование перегруженной функции с конкретными экземплярами классов типов, или явно, когда пользователь запрашивает её с помощью прагм; см. прагму SPECIALIZE и прагму SPECIALIZE для экземпляров.
Затем специализатор последовательно проверяет ряд условий, чтобы решить, выполнять ли специализацию. Ниже приведён наиболее точный из доступных на данный момент списков условий, проверяемых GHC.
- Если у какого-либо экземпляра класса типов есть аргументы типов, а
-fpolymorphic-specialisationне включён (по умолчанию он выключен), функция не будет специализирована; в противном случае - если специализация запрошена с помощью прагмы, GHC попытается создать специализацию; в противном случае
- если функция импортирована: + если развёртка недоступна, специализация функции невозможна; в противном случае + если
-fcross-module-specialiseне включён (по умолчанию он включён при-O), функция не будет специализирована; в противном случае + если флаг включён, но у функции нет прагмы INLINABLE/INLINE, функция не будет специализирована; в противном случае - если включён
-fspecialise-aggressively, GHC попытается создать специализацию; в противном случае - если перегруженная функция определена в текущем модуле и все экземпляры классов типов известны статически, она будет специализирована; в противном случае
- функция не будет специализирована.
Обратите внимание: в некоторых случаях GHC попытается специализировать функцию, но потерпит неудачу. Например, если у функции есть прагма OPAQUE или развёртка недоступна.
После специализации функции GHC создаёт правило, подобное правилам, создаваемым прагмами RULE. Оно срабатывает в местах вызова с известными экземплярами, заменяя, когда это возможно, вызовы перегруженной функции вызовами специализированной функции.