Spec-Zone.ru › Haskell 9

11. Советы

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

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, описанные отдельно в Профилирование. Нет замены поиску мест, где действительно тратится время/место программы, а не тех, где вы себе представляете.

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

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

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

Простой способ: используйте 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 лучше, чем datatypes:

Если ваш тип данных имеет один конструктор с одним полем, используйте объявление 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.

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

Если статистика сборки мусора вашей программы (-S [⟨file⟩] опция RTS) показывает, что она выполняет много сборок мусора (скажем, более 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 требует развертывания функций. Следующие флаги могут использоваться для управления созданием развертываний, делая их создание более или менее вероятным:

  • -funfolding-creation-threshold=⟨n⟩
  • -fexpose-overloaded-unfoldings
  • -fexpose-all-unfoldings

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. Встраивание обобщений

Существуют также флаги, специфичные для встраивания обобщений:

  • -finline-generics
  • -finline-generics-aggressively

11.6. Управление специализацией

GHC может оптимизировать полиморфный код для определённых экземпляров класса типов в месте использования. Мы называем это специализацией, и она включена с помощью -fspecialise, которая включена по умолчанию на -O1 или выше.

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

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

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

11.6.1. Общие комбинации флагов/пragм

Для приложений, которые не сильно ресурсоёмкие, значения по умолчанию часто достаточно хороши, так как они стремятся к разумному балансу между временем компиляции и временем выполнения.

Для библиотек, если экспортированные функции могут значительно выиграть от специализации, рекомендуется включить -fexpose-overloaded-unfoldings или вручную прикрепить pragмы INLINEABLE к функциям, имеющим отношение к производительности. Это обеспечит, что пользователи ниже по иерархии могут специализировать любые перегруженные функции, экспортируемые библиотекой, если это полезно.

Если существуют ключевые части приложения, которые полагаются на специализацию для повышения производительности, использование SPECIALIZE pragм в сочетании с -fexpose-overloaded-unfoldings или INLINEABLE для ключевых перегруженных функций должно позволить этим функциям специализироваться без чрезмерного влияния на общее время компиляции.

Для ресурсоёмкого кода, в котором важна минимизация накладных расходов, рекомендуется использовать комбинацию -fspecialise-aggressively и -fexpose-overloaded-unfoldings или -fexpose-all-unfoldings. Однако это влечёт за собой значительные затраты времени компиляции.

11.6.2. Доступность развертываний

Доступность развертываний в основном определяется этими флагами.

Особый интерес для специализации представляют:

  • -fexpose-all-unfoldings
  • -fexpose-overloaded-unfoldings

Первый делает все развертывания доступными, потенциально с большими затратами времени компиляции. Последний делает доступными только функции, которые перегружены. В целом лучше использовать -fexpose-overloaded-unfoldings вместо -fexpose-all-unfoldings, когда целью является обеспечение специализации.

11.6.3. Когда GHC генерирует специализации

Функции рассматриваются для специализации либо неявно, когда GHC видит использование перегруженной функции с конкретными экземплярами класса типов, либо явно, когда пользователь запрашивает это через pragмы, см. SPECIALIZE pragma и SPECIALIZE instance pragma.

Специализатор затем проверяет ряд условий в определённом порядке, чтобы решить, следует ли производить специализацию. Ниже приведён список условий, которые GHC проверяет в настоящее время.

  • Если у любого из экземпляров класса типов есть аргументы типов и -fpolymorphic-specialisation не включён (выключен по умолчанию), функция не будет специализирована; в противном случае
  • если специализация была запрошена через pragму, GHC попытается создать специализацию; в противном случае
  • если функция импортирована и: + если развертывание недоступно, функция не может быть специализирована; в противном случае + если -fcross-module-specialise не включён (включен по -O), функция не будет специализирована; в противном случае + если флаг включён, и у функции нет pragмы INLINABLE/INLINE, она не будет специализирована; в противном случае
  • если -fspecialise-aggressively включён, GHC попытается создать специализацию; в противном случае
  • если перегруженная функция определена в текущем модуле, и все экземпляры класса типов статически известны, она будет специализирована; в противном случае
  • функция не будет специализирована.

Обратите внимание, что существуют случаи, когда GHC попытается специализировать функцию и потерпит неудачу. Например, если у функции есть pragма OPAQUE или развертывание недоступно.

После специализации функции GHC создаст правило, подобное создаваемым pragмами RULE, которое будет срабатывать в местах вызова, связанных с известными экземплярами, заменяя вызовы перегруженной функции вызовами специализированной функции, когда это возможно.

11.7. Понимание того, как использование оперативной памяти ОС соответствует данным в памяти

Путающим аспектом RTS является иногда большая разница между использованием оперативной памяти, сообщенной ОС, и объёмом активных данных, сообщённых профилированием кучи или GHC.Stats.

Существует два основных фактора, определяющих использование оперативной памяти ОС.

Во-первых, стратегия сбора мусора, используемая старейшей генерацией. По умолчанию используется стратегия копирования, которая требует как минимум в 2 раза больше памяти, чем количество активных данных в настоящее время, для выполнения основного сбора мусора. Например, если активные данные вашей программы составляют 1 ГБ, то вы ожидаете, что ОС сообщит как минимум 2 ГБ.

Если вместо этого вы используете стратегии компактирования (-c) или неперемещения (-xn) для старейшей генерации, то требуется меньше накладных расходов, так как стратегия сразу повторно использует уже выделенную память, перезаписывая её. Для программы с размером кучи 1 ГБ вы можете ожидать, что ОС сообщит как минимум немного больше 1 ГБ.

Во-вторых, после выполнения некоторого выделения памяти GHC довольно неохотно возвращает память ОС. Это связано с тем, что после выполнения основного сбора мусора программа может по-прежнему выполнять много выделений, и затраты на запрос дополнительной памяти. Поэтому RTS сохраняет дополнительный объём для повторного использования, который зависит от параметра -F ⟨factor⟩. По умолчанию RTS будет сохранять до (2 + F) * live_bytes после выполнения основного сбора мусора из-за исчерпания доступной кучи. Значение по умолчанию равно F = 2, поэтому вы можете видеть, что использование памяти ОС сообщается как в 4 раза больше, чем количество, используемое вашей программой.

Без дополнительных вмешательств, как только ваша программа достигнет этого высокого порога, больше памяти не будет возвращаться ОС, поэтому использование памяти всегда будет оставаться в 4 раза больше, чем активные данные. Если у вас был сервер с 1,5 ГБ активных данных, а затем был скачок использования памяти до 6 ГБ на короткий период, то использование памяти ОС никогда не опустится ниже 6 ГБ. Это произошло до GHC 9.2. В GHC 9.2 память постепенно возвращается ОС, поэтому использование памяти ОС приближается к теоретическим минимумам.

Параметр -Fd ⟨factor⟩ управляет скоростью возврата памяти ОС. При последовательных основных сборах мусора, которые не вызываются переполнением кучи, счётчик (t) увеличивается, а фактор F обратно пропорционально масштабируется в зависимости от значения t и Fd. Фактор масштабируется по формуле:

\[\texttt{F}' = \texttt{F} \times {2 ^ \frac{- \texttt{t}}{\texttt{Fd}}}\]

По умолчанию Fd = 4, увеличение Fd уменьшает скорость возврата памяти.

Основные сборы мусора, которые не вызываются переполнением кучи, возникают в основном двумя способами.

  1. Простые сборы мусора (управляемые -I  ⟨seconds⟩)
  2. Явное срабатывание с помощью performMajorGC.

Например, простые сборы мусора по умолчанию происходят после 0,3 секунд бездействия. Если вы запускаете своё приложение и также задали -Iw30, чтобы минимальный период между простыми сборами мусора составлял 30 секунд, то, допустим, вы выполняете небольшую работу каждые 5 секунд, произойдёт около 10 простых сборов мусора примерно через 5 минут. Это количество последовательных простых сборов мусора приведет к масштабированию фактора F следующим образом:

\[\texttt{F}' = 2 \times {2^{\frac{-10}{4}}} \approx 0.35\]

и, следовательно, мы будем сохранять только (0.35 + 2) * live_bytes вместо исходных 4 раз. Если вы хотите менее частые простые сборы мусора, то вы также должны уменьшить Fd, чтобы в каждом сборе возвращалось больше памяти.

Если вы зададите -Fd0, то GHC не будет пытаться возвращать память, что соответствует поведению версий до 9.2. Вероятно, вам этого не нужно делать, поскольку, если в вашей программе нет периодов бездействия, поведение будет аналогичным. Если вы хотите сохранить определенный объём памяти, то лучше задать -H1G , чтобы указать, что вы довольны размером кучи 1G. Если вы это сделаете, использование памяти ОС никогда не уменьшится ниже этого значения, если оно когда-либо достигнет этого порога.

Стратегия сбора мусора также влияет на фрагментацию кучи и, следовательно, на то, насколько легко вернуть память к теоретическому базовому значению. Память выделяется сначала в единицах мегаблоков, которые затем делятся на блоки. Фрагментация на уровне блоков — это количество неиспользуемого места в выделенных мегаблоках. В фрагментированной куче будет много мегаблоков, которые заполнены только частично.

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

Существуют и другие флаги, которые влияют на количество удерживаемой памяти. Установка максимального размера кучи с помощью -M ⟨size⟩ гарантирует, что мы не будем пытаться удерживать больше памяти, чем максимальный размер, и явное задание -H [⟨size⟩] означает, что мы всегда будем пытаться удерживать как минимум H байт независимо от количества активных данных.

© 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/9.12.1/docs/users_guide/hints.html

Spec-Zone.ru

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