Spec-Zone.ru › Haskell 8

9. Советы по: быстрее, быстрее, меньше, экономнее

Пожалуйста, сообщите нам о других «полезных советах», которые должны быть здесь!

9.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 — ваш друг.

9.2. Быстрее: создание программы, которая работает быстрее

Ключевым инструментом для ускорения вашей программы Haskell являются средства профилирования GHC, описанные отдельно в Профилирование. Нет замены тому, чтобы выяснить, куда реально уходит время/память вашей программы, а не куда вы предполагаете.

Еще один момент, который следует учитывать: наилучшим способом значительно улучшить производительность программы является использование лучших алгоритмов. После того, как профилирование выявило виновника(ов) потребления времени, может быть лучше пересмотреть программу, чем пытаться внести все коррективы, перечисленные ниже.

Еще один чрезвычайно эффективный способ ускорения вашей программы — использование кода библиотек, который был серьезно настроен кем-то другим. Вы, возможно, сможете написать лучшую сортировку быстрой сортировки, чем та, что в 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:

Специализируйте перегрузку для ключевых функций в вашей программе. См. pragma SPECIALIZE и pragma SPECIALIZE instance.

«Но как узнать, где просачивается перегрузка?»

Примитивный способ: используйте grep (поиск) в ваших файлах интерфейса для перегруженных подписей типов. Вы можете просмотреть файлы интерфейса с помощью опции --show-iface ⟨file⟩ (см. Другие опции, связанные с файлами интерфейса).

$ ghc --show-iface Foo.hi | egrep '^[a-z].*::.*=>'
Строгие функции — ваши верные друзья:

И, помимо прочего, ленивый образец-сопоставление — ваш враг.

(Если вы не знаете, что такое «строгая функция», пожалуйста, обратитесь к учебнику по функциональному программированию. Предложение-два объяснений здесь вряд ли будут полезны.)

Рассмотрим эти два фрагмента кода:

f (Wibble x y) =  ... # strict

f arg = let { (Wibble x y) = arg } in ... # lazy

Первый фрагмент приведёт к гораздо более эффективному коду.

Менее искусственный пример показывает использование cases вместо lets для получения более строгого кода (что хорошо):

f (Wibble x y)  # beautiful but slow
      = let
            (a1, b1, c1) = unpackFoo x
            (a2, b2, c2) = unpackFoo y
        in ...

f (Wibble x y)  # ugly, and proud of it
      = case (unpackFoo x) of { (a1, b1, c1) ->
            case (unpackFoo y) of { (a2, b2, c2) ->
                ...
      }}
GHC любит типы данных с единственным конструктором:

Тем лучше, если функция строгая для типа с единственным конструктором (тип с только одним конструктором данных; например, кортежи — типы с единственным конструктором).

Newtypes лучше, чем datatypes:

Если ваш тип данных имеет единственный конструктор с единственным полем, используйте объявление newtype вместо объявления data. newtype будет оптимизирован в большинстве случаев.

«Как узнать строгую ли функция?»

Не догадывайтесь — ищите.

Найдите вашу функцию в файле интерфейса, затем найдите третье поле в прагме; оно должно сказать Strictness: ⟨string⟩. ⟨строка⟩ даёт строгую информацию о параметрах функции: см. комментарий GHC для описания обозначений строгости.

Для «распаковываемого» U(...) параметра информация внутри указывает строгую информацию о его компонентах. Так, если параметр — пара, и там написано U(AU(LSS)), это означает «первый компонент пары не используется; второй компонент сам по себе распаковывается с тремя компонентами (ленивый в первом, строгий во втором и третьем)».

Если функция не экспортирована, просто скомпилируйте с дополнительным флагом -ddump-simpl; рядом с подписью для любого связывателя будет выведена та же прагматическая информация, которая была бы помещена в файл интерфейса. (Кроме того, ядро синтаксиса интересно рассматривать!)

Force key functions to be INLINEd (esp. monads):

Размещение прагм INLINE на некоторых часто используемых функциях может иметь значительный эффект. См. pragma INLINE.

Explicit export list:

Если у вас нет явного списка экспорта в модуле, GHC должен предположить, что всё в этом модуле будет экспортировано. Это имеет различные негативные последствия. Например, если часть кода фактически _не используется_ (возможно, из-за эффектов развертывания), GHC не сможет её удалить, так как она экспортирована, и другой модуль может полагаться на её существование.

GHC может быть значительно агрессивнее с частями кода, если знает, что они не экспортируются.

Посмотрите на ядро синтаксиса!

(Форма, в которой GHC обрабатывает ваш код.) Просто запустите компиляцию с -ddump-simpl (не забудьте -O).

Если профилирование указало на определённые функции, посмотрите их код в ядре. lets — плохо, cases — хорошо, словари (d.⟨Class⟩.⟨Unique⟩) [или что-либо перегружающее] — плохо, вложенные лямбды — плохо, явные конструкторы данных — хорошо, примитивные операции (например, eqInt#) — хорошо, …

Используйте аннотации строгости:

Размещение аннотации строгости (!) на поле конструктора помогает по двум причинам: добавляет строгие элементы в программу, предоставляя анализатору строгости больше данных для работы, и может помочь уменьшить утечки памяти.

Также это может помочь и третим способом: при использовании с -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) описывает интерфейс внешних функций.

Don’t use Floats:

Если вы используете Complex, обязательно используйте Complex Double вместо Complex Float (первый сильно специализирован, а второй — нет).

Floats (вероятно, 32-битные) почти всегда плохая идея, если вы не точно знаете, что делаете. Используйте Double. Редко наблюдается недостаток скорости — современные машины будут использовать тот же блок вычисления с плавающей запятой для обоих. С Double вы гораздо меньше рискуете испортить себя численными ошибками.

Единственный случай, когда Float может быть хорошей идеей, — если у вас их _очень много_, скажем, огромный массив Float. Они занимают в два раза меньше места в куче по сравнению с Doubles. Однако это неверно на 64-битной машине.

Use unboxed arrays (UArray)

GHC поддерживает массивы из необрамлённых элементов для нескольких основных арифметических типов элементов, включая Int и Char: подробнее см. библиотеку Data.Array.Unboxed. Эти массивы, скорее всего, будут намного быстрее, чем использование стандартных массивов Haskell 98 из библиотеки Data.Array.

Используйте большую кучу!

Если статистика сборки мусора вашей программы (-S [⟨file⟩] RTS-опция) указывает на то, что она выполняет много сборок мусора (скажем, более 20% времени выполнения), больше памяти может помочь — с помощью -H [⟨size⟩] или -A ⟨size⟩ RTS-опций (см. RTS-опции для управления сборщиком мусора). Как правило, попробуйте установить -H [⟨size⟩] на объём памяти, который вы готовы предоставить вашему процессу, или возможно, попробуйте передать -H [⟨size⟩] без аргументов, чтобы GHC вычислил значение на основе объёма активных данных.

Компактность данных:

Модуль GHC.Compact предоставляет способ сделать сборку мусора более эффективной для долгоживущих структур данных. Компактирование структуры данных собирает объекты вместе в памяти, где они обрабатываются сборщиком мусора как единый объект, а не проходятся по отдельности.

9.3. Smaller: создание более компактной программы

Уменьшите порог «go-for-it» для разворачивания небольших выражений. Предоставьте -funfolding-use-threshold=0 вариант для крайнего случая. («Должны продолжаться только разворачивания с нулевой стоимостью».) Предупреждение: за исключением определённых специализированных случаев (например, парсеры Happy), это, вероятно, увеличит размер вашей программы, потому что разворачивание, как правило, позволяет выполнять дополнительные оптимизации упрощения.

Избегайте Prelude.Read.

Используйте strip для ваших исполняемых файлов.

9.4. Thriftier: создание программы, потребляющей меньше памяти

«Я думаю, у меня утечка памяти…»

Запустите вашу программу повторно с +RTS -S, и развейте все сомнения! (Вы увидите, как потребление памяти будет всё увеличиваться…) (Хмм… это может быть ещё проще с опцией RTS -G1; так что… ./a.out +RTS -S -G1)

Ещё раз, средства профилирования (Профилирование) являются основным инструментом для выяснения поведения потребления памяти вашей программы.

Строгие функции хороши для использования памяти, как и для времени, как обсуждалось в предыдущем разделе. Строгие функции сразу приступают к делу, а не заполняют кучу замыканиями (примечания системы о том, как оценить что-то, если это потребуется в конечном итоге).

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/8.10.2/docs/html/users_guide/sooner.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API