Spec-Zone.ru › OCaml 5.0

Глава 23 Оптимизация с помощью Flambda

  • 23.1 Обзор
  • 23.2 Флаги командной строки
  • 23.3 Встраивание
  • 23.4 Специализация
  • 23.5 Значения параметров по умолчанию
  • 23.6 Ручное управление встраиванием и специализацией
  • 23.7 Упрощение
  • 23.8 Другие преобразования перемещения кода
  • 23.9 Преобразования распаковки
  • 23.10 Удаление неиспользуемого кода и значений
  • 23.11 Другие преобразования кода
  • 23.12 Обработка эффектов
  • 23.13 Компиляция статически размещённых модулей
  • 23.14 Запрет оптимизации
  • 23.15 Использование небезопасных операций
  • 23.16 Словарь

23.1 Обзор

Flambda — это термин, используемый для обозначения ряда оптимизационных проходов, предоставляемых компиляторами машинного кода с момента OCaml 4.03.

Flambda призван упростить написание идиоматичного кода OCaml без потери производительности.

Для использования оптимизаторов Flambda необходимо передать параметр -flambda скрипту конфигурации OCaml configure. (Нет поддержки единого компилятора, который может работать как в режиме Flambda, так и без него.) Код, скомпилированный с Flambda, не может быть связан в одну программу с кодом, скомпилированным без Flambda. Попытка сделать это приведёт к ошибке компилятора.

Можно определить, использует ли конкретный ocamlopt Flambda, вызвав его с параметром -config и поиска любой строки, начинающейся с «flambda:». Если такая строка есть и содержит «true», то Flambda поддерживается, в противном случае — нет.

Flambda предоставляет полную оптимизацию по различным единицам компиляции, при условии, что файлы .cmx для зависимостей текущей компилируемой единицы доступны. (Единица компиляции соответствует одному исходному файлу .ml.) Однако он всё ещё не действует как полноценный компилятор всего проекта: например, удаление мёртвого кода по всему набору единиц компиляции не поддерживается.

Оптимизация с помощью Flambda в настоящее время не поддерживается при генерации байт-кода.

Как правило, Flambda не должен влиять на семантику существующих программ. Два исключения из этого правила: возможное удаление чистого кода, который тестируется (см. раздел 23.14) и изменения в поведении кода, использующего небезопасные операции (см. раздел 23.15).

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

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

23.2 Флаги командной строки

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

Часто используемые опции:

-O2
Выполнить больше оптимизаций, чем обычно. Время компиляции может увеличиться. (Этот флаг является сокращением определённого набора параметров, описанных в разделе 23.5.)
-O3
Выполнить ещё больше оптимизаций, чем обычно, возможно, включая развертывание рекурсивных функций. Время компиляции может значительно увеличиться.
-Oclassic
Принимать решения об инлайнинге в момент определения функции, а не в месте(ах) вызова. Это отражает поведение компиляторов OCaml, не использующих Flambda. По сравнению с компиляцией с использованием новых эвристик инлайнинга Flambda (например, при -O2), она производит файлы .cmx меньшего размера, сокращает время компиляции и генерирует код, который, вероятно, будет работать несколько медленнее. При использовании -Oclassic, актуальными являются только следующие опции, описанные в этом разделе: -inlining-report и -inline. Если используются какие-либо другие опции, описанные в этом разделе, поведение является неопределённым и может привести к ошибке в будущих версиях компилятора.
-inlining-report
Создавать файлы .inlining (по одному на раунд оптимизации), показывающие все решения инлайнера.

Реже используемые опции:

-remove-unused-arguments
Удалять неиспользуемые аргументы функций, даже если аргумент не специализирован. Это может немного повлиять на производительность. См. раздел 23.10.3.
-unbox-closures
Передавать свободные переменные через специализированные аргументы, а не через замыкания (оптимизация для уменьшения выделения памяти). См. раздел 23.9.3. Это может немного повлиять на производительность.

Дополнительно настраиваемые опции, необходимые только для тонкой настройки:

-inline
Поведение зависит от того, используется ли -Oclassic.
  • В режиме, отличном от -Oclassic, -inline ограничивает общий размер функций, рассматриваемых для инлайнинга во время любого спекулятивного поиска инлайнинга. (См. раздел 23.3.6.) Обратите внимание, что этот параметр не управляет оценкой того, может ли та или иная функция быть инлайнирована. Его избыточное увеличение не обязательно приведёт к инлайнингу большего числа функций.
  • В режиме -Oclassic, -inline ведет себя как в предыдущих версиях компилятора: он является максимальным размером функции, которая должна рассматриваться для инлайнинга. См. раздел 23.3.1.
-inline-toplevel
Эквивалент -inline, но используется, когда спекулятивный инлайнинг начинается на уровне верхнего уровня. См. раздел 23.3.6. Не используется в режиме -Oclassic.
-inline-branch-factor
Управляет тем, как инлайнер оценивает, является ли путь кода вероятным "горячим" или "холодным". См. раздел 23.3.5.
-inline-alloc-cost, -inline-branch-cost, -inline-call-cost
Управляет тем, как инлайнер оценивает штрафы за время выполнения, связанные с различными операциями. См. раздел 23.3.5.
-inline-indirect-cost, -inline-prim-cost
Аналогично.
-inline-lifting-benefit
Управляет инлайнингом функторов на уровне верхнего уровня. См. раздел 23.3.5.
-inline-max-depth
Максимальная глубина любого спекулятивного поиска инлайнинга. См. раздел 23.3.6.
-inline-max-unroll
Максимальная глубина любого развертывания рекурсивных функций во время любого спекулятивного поиска инлайнинга. См. раздел 23.3.6.
-no-unbox-free-vars-of-closures
Не распаковывать переменные замыканий. См. раздел 23.9.1.
-no-unbox-specialised-args
Не распаковывать аргументы, к которым функции были специализированы. См. раздел 23.9.2.
-rounds
Количество раундов оптимизации для выполнения. См. раздел 23.2.1.
-unbox-closures-factor
Масштабирующий коэффициент для расчёта выгоды при использовании -unbox-closures. См. раздел 23.9.3.
Примечания
  • Набор флагов командной строки, относящихся к оптимизации, обычно должен быть одинаковым для всего проекта. Flambda в настоящее время не записывает запрошенные флаги в файлы .cmx. Таким образом, встраивание функций из ранее скомпилированных модулей будет подчинять их код параметрам оптимизации модуля, который в данный момент компилируется, а не тем, которые были указаны при их предыдущей компиляции. Надеемся исправить этот недостаток в будущем.
  • Флаги, специфичные для Flambda, не влияют на компоновку, за исключением влияния на оптимизацию кода в стартовом файле (содержащем сгенерированные функции, такие как вспомогательные функции кэширования). Обычно такая оптимизация не будет значительной, поэтому исключение таких флагов во время компоновки может быть оправданным.
  • Флаги, специфичные для Flambda, молча принимаются даже в том случае, если опция -flambda не была указана в скрипте configure. (Нет возможности изменить это поведение.) Это призвано упростить запуск бенчмарков с включенными и выключенными оптимизаторами Flambda.
  • Некоторые флаги Flambda могут быть изменены в будущих выпусках.

23.2.1 Указание параметров оптимизации по раундам

Flambda работает по раундам: один раунд состоит из определенной последовательности преобразований, которые затем могут быть повторены для достижения более удовлетворительных результатов. Количество раундов можно задать вручную с помощью параметра -rounds (хотя это не обязательно при использовании предопределенных уровней оптимизации, таких как -O2 и -O3). Для высокой оптимизации количество раундов может быть установлено на 3 или 4.

Флаги командной строки, которые могут применяться в каждом раунде, например, те, у которых в имени есть -cost, принимают аргументы в формате:

n | round=n[,...]
  • Если используется первый формат с одним целым числом, значение будет применяться ко всем раундам.
  • Если используется второй формат, целочисленные значения round (с нулевой базой) определяют значения, которые будут использоваться только для этих раундов.

Флаги -Oclassic, -O2 и -O3 применяются до всех остальных флагов, что означает, что некоторые параметры могут быть переопределены без необходимости указывать каждый параметр, обычно вызываемый заданным уровнем оптимизации.

23.3 Встраивание

Встраивание относится к копированию кода функции в место, где функция вызывается. Код функции будет окружен привязками её параметров к соответствующим аргументам.

Цели встраивания:

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

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

Когда рекурсивный вызов функции (внутри определения этой функции или другой в той же группе взаимно-рекурсивных функций) встраивается, процедура также известна как раскрутка. Это чем-то похоже на удаление цикла. Например, задан следующий код:

let rec fact x =
  if x = 0 then
    1
  else
    x * fact (x - 1)

let n = fact 4

раскрутка один раз в месте вызова fact 4 даёт (при неизменном теле fact):

let n =
  if 4 = 0 then
    1
  else
    4 * fact (4 - 1)

Это упрощается до:

let n = 4 * fact 3

Flambda предоставляет значительно расширенные возможности встраивания по сравнению с предыдущими версиями компилятора.

Примечание: когда выполняется встраивание

Встраивание выполняется вместе со всеми другими оптимизационными проходами Flambda, то есть после преобразования замыканий. Это имеет три конкретных преимущества по сравнению с потенциально более простым внедрением до преобразования замыканий:

  • Это позволяет встраивание высших порядков, например, когда не встраиваемая функция всегда возвращает ту же функцию, но с различными средами определения. Не все такие случаи поддерживаются пока, но предполагается, что такая поддержка будет улучшена в будущем.
  • Это проще интегрировать с межмодульной оптимизацией, так как информация о других модулях уже в правильном промежуточном языке.
  • Это становится проще оптимизировать выделение памяти под замыкания, так как макет замыканий не нужно оценивать каким-либо образом: он известен. Точно так же становится проще управлять тем, какие переменные оказываются в каких замыканиях, помогая избежать чрезмерного использования памяти под замыкания.

23.3.1 Эвристика классического встраивания

В режиме -Oclassic поведение встраивателя Flambda имитирует предыдущие версии компилятора. (Код все ещё может быть подвержен дальнейшим оптимизациям, которые не выполнялись предыдущими версиями компилятора: могут быть встроены функторы, константы подняты наверх, и удалён неиспользуемый код, как описано в других разделах этой главы. См. разделы 23.3.3, 23.8.1 и 23.10. В месте определения функции измеряется размер тела функции. Затем она будет помечена как подходящая для встраивания (и, следовательно, встроена в каждом месте прямого вызова), если:

  • измеренный размер (в неуказанных единицах) меньше, чем размер вызова функции плюс аргумент флага командной строки -inline; и
  • функция не рекурсивна.

Не-Flambda версии компилятора не могут встраивать функции, которые содержат определение другой функции. Однако -Oclassic это позволяет. Кроме того, не-Flambda версии также не могут встраивать функции, которые являются только результатом предыдущего прохода встраивания, но опять же это разрешено -Oclassic. Например:

module M : sig
  val i : int
end = struct
  let f x =
    let g y = x + y in
    g
  let h = f 3
  let i = h 4  (* h is correctly discovered to be g and inlined *)
end

Всё это контрастирует с обычным режимом Flambda, то есть без -Oclassic, где:

  • решение о встраивании принимается в месте вызова; и
  • рекурсивные функции могут обрабатываться путём специализации (см. ниже).

Режим Flambda описан в следующем разделе.

23.3.2 Обзор эвристик встраивания «Flambda»

Эвристики встраивания Flambda, используемые всякий раз, когда компилятор настроен на Flambda и не был указан -Oclassic, принимают решения о встраивании в местах вызова. Это помогает в ситуациях, когда контекст важен. Например:

let f b x =
  if b then
    x
  else
    ... big expression ...

let g x = f true x

В этом случае мы хотим встроить f в g, потому что условный переход можно исключить, а размер кода должен уменьшиться. Если решение о встраивании принято после объявления f без просмотра использования, его размер, вероятно, сделал бы его непригодным для встраивания; но в месте вызова его окончательный размер может быть известен. Кроме того, эту функцию, вероятно, не следует систематически встраивать: если b неизвестно или, действительно, равно false, польза небольшая, чтобы торговаться против большого увеличения размера кода. В существующем не-Flambda встраивателе это не большая проблема, потому что цепочки встраивания обрывались довольно быстро. Однако это привело к чрезмерному использованию чрезмерно больших параметров встраивания, таких как -inline 10000.

Более подробно, в каждом месте вызова выполняется следующая процедура:

  • Определить, очевидно ли, что встраивание будет полезным без предварительного выполнения встраивания внутри самой функции. (Точное определение пользы описано ниже.) Если это так, функция встраивается.
  • Если встраивание функции не очевидно полезно, то встраивание будет выполнено спекулятивно внутри самой функции. Поиск возможностей спекулятивного встраивания контролируется двумя параметрами: порогом встраивания и глубиной встраивания. (Эти параметры описаны более подробно ниже.)
    • Если такая спекуляция показывает, что выполнение некоторого встраивания внутри функции было бы полезным, то такое встраивание выполняется, и результирующая функция встраивается в исходном месте вызова.
    • В противном случае ничего не происходит.

Встраивание внутри рекурсивных функций вызовов других функций в той же группе взаимно-рекурсивных функций контролируется глубиной раскрутки, описанной ниже. Это гарантирует, что функции не раскручиваются чрезмерно. (Раскрутка включена только если используется уровень оптимизации -O3 и/или флаг -inline-max-unroll передан с аргументом больше нуля).

23.3.3 Обработка конкретных конструкций языка

Функторы

Нет ничего особенного в функторах, что препятствует их встраиванию по сравнению с обычными функциями. Для встраивателя они выглядят одинаково, за исключением того, что функторы помечены как таковые.

Применение функторов на верхнем уровне смещено в сторону встраивания. (Эта тенденция может быть скорректирована: см. документацию для -inline-lifting-benefit ниже).

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

Модули первого класса

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

Объекты

Вызовы методов к объектам в настоящее время не инлайнируются Flambda.

23.3.4 Отчёты об инлайнировании

Если параметр -inlining-report предоставляется компилятору, то будет создан файл, соответствующий каждому раунду оптимизации. Для файла исходного кода OCaml basename.ml файлы называются basename.round.inlining.org, где round — целое число с нулевой базой. Внутри файлов, отформатированных как «org mode», будет содержаться английский текст, описывающий решения, принятые инлайнером.

23.3.5 Оценка выгоды от инлайнирования

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

Например, для следующего кода:

let f b x =
  if b then
    x
  else
    ... big expression ...

let g x = f true x

будет замечено, что инлайнирование f удалит:

  • один прямой вызов;
  • одну условную ветвь.

Формально, оценка выгоды от производительности во время выполнения вычисляется путём сначала суммирования стоимости операций, которые, как известно, удаляются в результате инлайнирования и последующего упрощения тела инлайнированного фрагмента. Индивидуальные затраты на различные виды операций могут быть скорректированы с помощью различных флагов -inline-...-cost следующим образом. Стоимости указываются как целые числа. Все эти флаги принимают один аргумент, описывающий такие целые числа, используя соглашения, описанные в разделе 23.2.1.

-inline-alloc-cost
Стоимость выделения.
-inline-branch-cost
Стоимость ветвления.
-inline-call-cost
Стоимость прямого вызова функции.
-inline-indirect-cost
Стоимость косвенного вызова функции.
-inline-prim-cost
Стоимость примитива. Примитивы охватывают операции, включая арифметику и доступ к памяти.

(Значения по умолчанию описаны в разделе 23.5 ниже.)

Начальное значение выгоды затем масштабируется с помощью коэффициента, который пытается компенсировать тот факт, что текущая точка в коде, если находится под некоторым количеством условных ветвлений, может быть холодной. (Flambda в настоящее время не вычисляет горячие и холодные пути.) Коэффициент — оценённая вероятность того, что инлайнер действительно находится на горячем пути — вычисляется как 1/(1 + f)d, где f задаётся параметром -inline-branch-factor, а d — глубина вложения ветвлений в текущей точке. По мере того, как инлайнер спускается в более глубоко вложенные ветвления, выгода от инлайнирования уменьшается.

Полученное значение выгоды известно как ожидаемая выгода.

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

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

Приложения функторов на верхнем уровне будут получать дополнительную выгоду (которую можно контролировать с помощью флага -inline-lifting-benefit), чтобы сместить инлайнирование в таких ситуациях в сторону сохранения инлайнированной версии.

23.3.6 Управление спекуляцией

Как описано выше, существуют три параметра, которые ограничивают поиск возможностей инлайнирования во время спекуляции:

  • порог инлайнирования;
  • глубина инлайнирования;
  • глубина разворачивания.

Эти параметры в конечном итоге ограничены аргументами, предоставленными соответствующим флагам командной строки (или их значениям по умолчанию):

  • -inline (или, если место вызова, спровоцировавшего спекуляцию, находится на верхнем уровне, -inline-toplevel);
  • -inline-max-depth;
  • -inline-max-unroll.

Обратите особое внимание, что -inline не имеет того же значения, что и в предыдущем компиляторе или в режиме -Oclassic. В обеих этих ситуациях -inline фактически являлся своего рода базовой оценкой выгоды от инлайнирования. Однако в режиме инлайнирования Flambda он соответствует ограничению поиска; оценка выгоды независима, как описано выше.

Когда начинается спекуляция, порог инлайнирования устанавливается в значение, заданное параметром -inline (или -inline-toplevel, если это уместно, см. выше). При принятии решения о спекулятивном инлайнировании порог уменьшается на размер кода инлайнируемой функции. Если порог исчерпан, то есть становится или становится меньше нуля, дальнейшая спекуляция не будет производиться.

Глубина инлайнирования начинается с нуля и увеличивается на единицу каждый раз, когда инлайнер спускается в другую функцию. Затем она уменьшается на единицу каждый раз, когда инлайнер покидает такую функцию. Если глубина превышает значение, заданное параметром -inline-max-depth, спекуляция прекращается. Этот параметр предназначен в качестве общего стопора для ситуаций, когда порог инлайнирования недостаточно эффективно контролирует поиск.

Глубина разворачивания применяется к вызовам внутри одной и той же взаимно рекурсивной группы функций. Каждый раз, когда выполняется инлайнирование такого вызова, глубина увеличивается на единицу при рассмотрении результирующего тела. Если глубина достигает предела, установленного параметром -inline-max-unroll, спекуляция прекращается.

23.4 Специализация

Инлайнер может обнаружить место вызова рекурсивной функции, где что-то известно об аргументах: например, они могут быть равны некоторым другим переменным, в настоящее время находящимся в области видимости. В этой ситуации может быть выгодно специализировать функцию по этим аргументам. Это делается путём копирования объявления функции (и всех других, участвующих в любом таком взаимно рекурсивном объявлении) и заменой информации об аргументах. Аргументы, дополненные этой информацией, известны как специализированные аргументы. Для того, чтобы попытаться гарантировать, что специализация не выполняется бесполезно, аргументы специализируются только в том случае, если можно показать, что они инвариантны: другими словами, в процессе выполнения рекурсивной функции(й), аргументы никогда не изменяются.

Если не переопределено атрибутом (см. ниже), специализация функции не будет предпринята, если:

  • компилятор находится в режиме -Oclassic;
  • функция не является очевидно рекурсивной;
  • функция не закрыта.

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

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

Пример: хорошо известная функция List.iter

Эта функция может быть написана следующим образом:

let rec iter f l =
  match l with
  | [] -> ()
  | h :: t ->
    f h;
    iter f t

и использоваться так:

let print_int x =
  print_endline (Int.to_string x)

let run xs =
  iter print_int (List.rev xs)

Аргумент f для iter инвариантен, поэтому функция может быть специализирована:

let run xs =
  let rec iter' f l =
    (* The compiler knows: f holds the same value as foo throughout iter'. *)
    match l with
    | [] -> ()
    | h :: t ->
      f h;
      iter' f t
  in
  iter' print_int (List.rev xs)

Компилятор отмечает, что для функции iter’ аргумент f специализирован по константному замыканию print_int. Это означает, что тело iter’ может быть упрощено:

let run xs =
  let rec iter' f l =
    (* The compiler knows: f holds the same value as foo throughout iter'. *)
    match l with
    | [] -> ()
    | h :: t ->
      print_int h;  (* this is now a direct call *)
      iter' f t
  in
  iter' print_int (List.rev xs)

Вызов print_int действительно может быть инлайнирован:

let run xs =
  let rec iter' f l =
    (* The compiler knows: f holds the same value as foo throughout iter'. *)
    match l with
    | [] -> ()
    | h :: t ->
      print_endline (Int.to_string h);
      iter' f t
  in
  iter' print_int (List.rev xs)

Неиспользуемый специализированный аргумент f теперь можно удалить, что оставит:

let run xs =
  let rec iter' l =
    match l with
    | [] -> ()
    | h :: t ->
      print_endline (Int.to_string h);
      iter' t
  in
  iter' (List.rev xs)
Отступление по инвариантным параметрам.

В настоящее время компилятор не может обнаружить инвариантность в таких случаях, как следующий.

let rec iter_swap f g l =
  match l with
  | [] -> ()
  | 0 :: t ->
    iter_swap g f l
  | h :: t ->
    f h;
    iter_swap f g t

23.4.1 Оценка выгоды от специализации

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

23.5 Параметры по умолчанию

Настройки по умолчанию (при отсутствии -Oclassic) предполагают один раунд оптимизации с использованием следующих параметров.

Параметр Значение
-inline 10
-inline-branch-factor 0.1
-inline-alloc-cost 7
-inline-branch-cost 5
-inline-call-cost 5
-inline-indirect-cost 4
-inline-prim-cost 3
-inline-lifting-benefit 1300
-inline-toplevel 160
-inline-max-depth 1
-inline-max-unroll 0
-unbox-closures-factor 10

23.5.1 Настройки при уровне оптимизации -O2

При указании -O2 выполняется два этапа оптимизации. Первый этап использует параметры по умолчанию (см. выше). Второй этап использует следующие параметры.

Параметр Значение
-inline 25
-inline-branch-factor То же, что и по умолчанию
-inline-alloc-cost Удвоенное значение по умолчанию
-inline-branch-cost Удвоенное значение по умолчанию
-inline-call-cost Удвоенное значение по умолчанию
-inline-indirect-cost Удвоенное значение по умолчанию
-inline-prim-cost Удвоенное значение по умолчанию
-inline-lifting-benefit То же, что и по умолчанию
-inline-toplevel 400
-inline-max-depth 2
-inline-max-unroll То же, что и по умолчанию
-unbox-closures-factor То же, что и по умолчанию

23.5.2 Настройки при уровне оптимизации -O3

При указании -O3 выполняется три этапа оптимизации. Первые два этапа аналогичны этапам для -O2. Третий этап использует следующие параметры.

Параметр Значение
-inline 50
-inline-branch-factor То же, что и по умолчанию
-inline-alloc-cost Тройное значение по умолчанию
-inline-branch-cost Тройное значение по умолчанию
-inline-call-cost Тройное значение по умолчанию
-inline-indirect-cost Тройное значение по умолчанию
-inline-prim-cost Тройное значение по умолчанию
-inline-lifting-benefit То же, что и по умолчанию
-inline-toplevel 800
-inline-max-depth 3
-inline-max-unroll 1
-unbox-closures-factor То же, что и по умолчанию

23.6 Ручное управление встраиванием и специализацией

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

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

@@inline always или @@inline never
Прикрепленные к объявлению функции или функтора, они указывают встраивателю всегда или никогда не встраивать функцию, независимо от расчета размера/выгоды. (Если функция рекурсивна, то тело подставляется, и никаких специальных действий для сайтов рекурсивных вызовов не выполняется.) @@inline без аргумента эквивалентно @@inline always.
@inlined always или @inlined never
Прикрепленные к применению функции, они указывают встраивателю аналогичным образом. Эти атрибуты на сайтах вызовов переопределяют любые другие атрибуты, которые могут присутствовать в соответствующем объявлении. @inlined без аргумента эквивалентно @inlined always. @@inlined hint эквивалентно @@inline always, за исключением того, что это не приведет к предупреждению 55, если применение функции нельзя встроить.

Для рекурсивных функций соответствующие атрибуты:

@@specialise always или @@specialise never
Прикрепленные к объявлению функции или функтора, они указывают встраивателю всегда или никогда не специализировать функцию, если у него есть соответствующие контекстные знания, независимо от расчета размера/выгоды. @@specialise без аргумента эквивалентно @@specialise always.
@specialised always или @specialised never
Прикрепленные к применению функции, они указывают встраивателю аналогичным образом. Этот атрибут на сайте вызова переопределяет любые другие атрибуты, которые могут присутствовать в соответствующем объявлении. (Обратите внимание, что функция все равно будет специализирована только в том случае, если существует один или несколько инвариантных параметров, значения которых известны.) @specialised без аргумента эквивалентно @specialised always.
@unrolled n
Этот атрибут прикреплен к применению функции и всегда принимает целочисленный аргумент. Каждый раз, когда встраиватель видит атрибут, он действует следующим образом:
  • Если n равно нулю или меньше, ничего не происходит.
  • В противном случае вызываемая функция подставляется в точке вызова своим телом, переписанным таким образом, что все рекурсивные вызовы этой функции или любые другие в той же группе взаимно рекурсивных функций помечены атрибутом unrolled(n − 1). Встраивание может продолжиться в этом теле.
Таким образом, n является «максимальной глубиной развертывания».

Если обнаружено, что невозможно выполнить аннотацию из атрибута @inlined или @specialised, будет выведено предупреждение компилятора.

Пример правильного размещения атрибутов
module F (M : sig type t end) = struct
  let[@inline never] bar x =
    x * 3

  let foo x =
    (bar [@inlined]) (42 + x)
end [@@inline never]

module X = F [@inlined] (struct type t = int end)

23.7 Упрощение

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

Распространение может помочь в устранении выделений в таких случаях, как:

let f x y =
  ...
  let p = x, y in
  ...
  ... (fst p) ... (snd p) ...

Проекции из p могут быть заменены использованием переменных x и y, что потенциально означает, что p станет неиспользуемым.

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

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

23.8 Другие преобразования перемещения кода

23.8.1 Поднятие констант

Выражения, которые оказываются константами, будут подняты до связываний символов — то есть, они будут статически размещены в объектном файле — когда они вычисляются до упакованных значений. Такие константы могут быть простыми числовыми константами, например, число с плавающей точкой 42.0, или более сложными значениями, такими как константные замыкания.

Поднятие констант в верхний уровень уменьшает выделение памяти во время выполнения.

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

Примечания об массивах чисел с плавающей точкой

Следующие семантики языка применимы конкретно к константным массивам чисел с плавающей точкой. (Под «константным массивом чисел с плавающей точкой» подразумевается массив, состоящий полностью из чисел с плавающей точкой, известных на этапе компиляции. Общим случаем является литерал, такой как [| 42.0; 43.0; |].

  • Константные массивы чисел с плавающей точкой на верхнем уровне являются изменяемыми и никогда не разделяются. (То есть, для каждого такого определения существует отдельный символ в разделе данных объектного файла, указывающий на массив.)
  • Константные массивы чисел с плавающей точкой не на верхнем уровне являются изменяемыми и создаются каждый раз при вычислении выражения. Это можно представить как операцию, которая принимает неизменяемый массив (который в исходном коде не имеет связанного имени; назовем его инициализирующим массивом) и дублирует его в новый изменяемый массив.
    • Если массив имеет размер четыре или меньше, выражение создаст новый блок и запишет значения в него по одному. Нет ссылки на инициализирующий массив в целом.
    • В противном случае, инициализирующий массив поднимается наверх и подвергается стандартной процедуре совместного использования констант; создание массива состоит из массового копирования инициализирующего массива в новое значение в куче OCaml.

23.8.2 Поднятие связываний верхнего уровня let

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

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

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

23.9 Преобразования распаковки

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

23.9.1 Распаковка переменных замыканий

Это преобразование включено, если не указано -no-unbox-free-vars-of-closures.

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

Пример:

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

let f x0 x1 =
  let pair = x0, x1 in
  Printf.printf "foo\n";
  fun y ->
    fst pair + snd pair + y

После некоторых упрощений получаем:

let f x0 x1 =
  let pair_0 = x0 in
  let pair_1 = x1 in
  Printf.printf "foo\n";
  fun y ->
    pair_0 + pair_1 + y

и затем:

let f x0 x1 =
  Printf.printf "foo\n";
  fun y ->
    x0 + x1 + y

Выделение памяти для пары было устранено.

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

23.9.2 Распаковка специализированных аргументов

Это преобразование включено, если не указано -no-unbox-specialised-args.

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

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

Пример:

Учитывая приведенный код, компилятор встроит loop в f, а затем обнаружит, что inv является инвариантным и всегда представляет собой пару, образованную путем добавления 42 и 43 к аргументу x функции f.

let rec loop inv xs =
  match xs with
  | [] -> fst inv + snd inv
  | x::xs -> x + loop2 xs inv
and loop2 ys inv =
  match ys with
  | [] -> 4
  | y::ys -> y - loop inv ys

let f x =
  Printf.printf "%d\n" (loop (x + 42, x + 43) [1; 2; 3])

Поскольку функции имеют достаточно мало аргументов, будут добавлены более специализированные аргументы. После некоторых упрощений получаем:

let f x =
  let rec loop' xs inv_0 inv_1 =
    match xs with
    | [] -> inv_0 + inv_1
    | x::xs -> x + loop2' xs inv_0 inv_1
  and loop2' ys inv_0 inv_1 =
    match ys with
    | [] -> 4
    | y::ys -> y - loop' ys inv_0 inv_1
  in
  Printf.printf "%d\n" (loop' [1; 2; 3] (x + 42) (x + 43))

Выделение памяти для пары внутри f было удалено. (Поскольку два замыкания для loop’ и loop2’ являются константами, они также будут подняты на верхний уровень без накладных расходов на выделение памяти во время выполнения. Это также произошло бы и без выполнения преобразования распаковки специализированных аргументов.)

Преобразование распаковки специализированных аргументов никогда не приводит к дополнительной выделению памяти.

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

Преобразование реализуется путем создания обертывающей функции, которая принимает исходные аргументы. В то же время исходная функция переименовывается, и добавляются дополнительные аргументы, соответствующие распакованным специализированным аргументам; эта новая функция вызывается из обертки. Затем обертка будет внедрена в прямые места вызова. Действительно, все места вызова будут прямыми, если не используется -unbox-closures, поскольку они были сгенерированы компилятором при первоначальной специализации функции. (В случае -unbox-closures могут появиться другие функции со специализированными аргументами; в этом случае могут быть косвенные вызовы, и это повлечет за собой небольшую накладную стоимость, так как придется пройти через оберточную функцию. Техника прямых суррогатов вызовов, используемая для -unbox-closures, не используется преобразованием распаковки специализированных аргументов.)

23.9.3 Распаковка замыканий

Это преобразование не включено по умолчанию. Оно может быть включено с помощью флага -unbox-closures.

Преобразование заменяет переменные замыканий специализированными аргументами. Цель состоит в том, чтобы заставить больше замыканий стать замкнутыми. Оно особенно применимо в качестве средства сокращения выделения памяти, где рассматриваемая функция не может быть встроена или специализирована. Например, некоторые нерекурсивные функции могут быть слишком большими для встраивания; или некоторые рекурсивные функции могут не предоставлять возможностей для специализации, возможно, потому, что их единственным аргументом является тип unit.

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

Простой пример:

В следующем коде (который, как правило, возникает, когда g слишком большой для встраивания) значение x обычно передается в применение функции + через замыкание g.

let f x =
  let g y =
    x + y
  in
  (g [@inlined never]) 42

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

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

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

Флаг командной строки -unbox-closures-factor, принимающий целое число, может использоваться для регулировки точки, в которой функция считается достаточно большой, чтобы не подходить для дублирования. Преимущество дублирования масштабируется на целочисленное значение перед оценкой по отношению к размеру.

Более сложный пример:

В следующем коде есть две переменные замыкания, которые обычно приводят к выделению памяти для замыканий. Одна называется fv и встречается внутри функции baz; другая называется z и встречается внутри функции bar. В этом игрушечном (но все же изощренном) примере мы снова используем атрибут для имитации типичной ситуации, когда первый аргумент функции baz слишком велик для встраивания.

let foo c =
  let rec bar zs fv =
    match zs with
    | [] -> []
    | z::zs ->
      let rec baz f = function
        | [] -> []
        | a::l -> let r = fv + ((f [@inlined never]) a) in r :: baz f l
      in
      (map2 (fun y -> z + y) [z; 2; 3; 4]) @ bar zs fv
  in
  Printf.printf "%d" (List.length (bar [1; 2; 3; 4] c))

Код, полученный в результате применения -O3 -unbox-closures к этому коду, передает свободные переменные через аргументы функций для устранения всего выделения памяти для замыканий в этом примере (кроме тех, которые могут выполняться внутри printf).

23.10 Удаление неиспользуемого кода и значений

23.10.1 Удаление избыточных выражений let

Этап упрощения удаляет неиспользуемые связи let, при условии, что соответствующие определяющие выражения не имеют «эффектов». См. раздел «Обработка эффектов» ниже для точного определения этого термина.

23.10.2 Удаление избыточных конструкций программы

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

23.10.3 Удаление неиспользуемых аргументов

Это преобразование по умолчанию включено только для специализированных аргументов. Его можно включить для всех аргументов, используя флаг -remove-unused-arguments.

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

23.10.4 Удаление неиспользуемых переменных замыканий

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

23.11 Другие преобразования кода

23.11.1 Преобразование невыходящих ссылок в изменяемые переменные

Flambda выполняет простой анализ, аналогичный тому, что выполняется в других частях компилятора, который может преобразовывать ссылки ref в изменяемые переменные, которые затем могут храниться в регистрах (или в стеке, как это уместно), а не выделяться в куче OCaml. Это происходит только в том случае, если можно показать, что соответствующая ссылка не выходит за пределы своего определяющего области.

23.11.2 Подстановка переменных замыканий специализированными аргументами

Это преобразование обнаруживает переменные замыканий, которые известны как равные специализированным аргументам. Такие переменные замыканий заменяются специализированными аргументами; переменные замыканий затем могут быть удалены этапом «удаления неиспользуемых переменных замыканий» (см. ниже).

23.12 Обработка эффектов

Оптимизаторы Flambda классифицируют выражения, чтобы определить, может ли выражение:

  • не нуждаться в вычислении вообще; и/или
  • подлежать дублированию.

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

Эффекты классифицируются следующим образом:

Без эффектов:
Выражение не изменяет наблюдаемого состояния мира. Например, оно не должно записывать в какую-либо изменяемую память, вызывать произвольные внешние функции или изменять поток управления (например, путём возбуждения исключения). Обратите внимание, что выделение памяти не считается «без эффектов» (см. ниже).
  • В компиляторе предполагается, что выражения без эффектов, результаты которых не используются, могут быть удалены. (Это обычно происходит, когда рассматриваемое выражение является определяющим выражением let; в таких случаях выражение let будет удалено.) Кроме того, предполагается, что такие выражения без эффектов могут быть дублированы (и, следовательно, возможно, выполнены более одного раза).
  • Исключения, возникающие из точек выделения памяти, например, «недостаточно памяти» или исключения, распространяемые из финализаторов или обработчиков сигналов, обрабатываются как «эффекты из ниоткуда» и, следовательно, игнорируются для нашего определения здесь эффективности. То же самое относится к операциям с плавающей точкой, которые могут вызвать аппаратные ловушки на некоторых платформах.
Только эффекты генерации:
Выражение не изменяет наблюдаемого состояния мира, за исключением, возможно, влияния на состояние сборщика мусора путём выделения памяти. Выражения, имеющие только эффекты генерации и результаты которых не используются, могут быть удалены компилятором. Однако, в отличие от выражений «без эффектов», такие выражения никогда не будут подходить для дублирования.
Произвольные эффекты:
Все остальные выражения.

Существует одна классификация для побочных эффектов:

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

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

23.13 Компиляция статически размещенных модулей

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

23.14 Ингибирование оптимизации

Особенно при написании наборов бенчмарков, выполняющих не имеющие побочных эффектов алгоритмы в циклах, может оказаться, что оптимизатор полностью исключает тестируемый код. Это поведение можно предотвратить, используя функцию Sys.opaque_identity (которая действительно ведет себя как обычная функция OCaml и не обладает никакой «магической» семантикой). Для получения дополнительной информации следует обратиться к документации модуля Sys.

23.15 Использование небезопасных операций

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

В частности, запрещается изменять любые значения (например, используя Obj.set_field или Obj.set_tag), которые не являются изменяемыми. (Значения, возвращаемые из C-стыков, всегда обрабатываются как изменяемые.) Компилятор выведет предупреждение 59, если обнаружит такую запись, но не может предупредить во всех случаях. Вот пример кода, который вызовет предупреждение:

let f x =
  let a = 42, x in
  (Obj.magic a : int ref) := 1;
  fst a

Причина, по которой это небезопасно, заключается в том, что проход упрощения считает, что fst a хранит значение 42; и, действительно, так оно и должно быть, если целостность типов не нарушена с помощью небезопасных операций.

Если код должен быть написан таким образом, чтобы вызывать предупреждение 59, но код, как известно, фактически корректен (для некоторого определения корректности), то Sys.opaque_identity можно использовать для обертывания значения перед выполнением небезопасных операций над ним. Необходимо проявлять особую осторожность при этом, чтобы убедиться, что непрозрачность добавлена в нужном месте. Следует подчеркнуть, что это использование Sys.opaque_identity предназначено только для исключительных случаев. Его не следует использовать в обычном коде или для попыток направлять оптимизатор.

Например, этот код вернёт целое число 1:

let f x =
  let a = Sys.opaque_identity (42, x) in
  (Obj.magic a : int ref) := 1;
  fst a

Однако следующий код всё равно вернёт 42:

let f x =
  let a = 42, x in
  Sys.opaque_identity (Obj.magic a : int ref) := 1;
  fst a

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

23.16 Словарь терминов

В этой главе руководства используется следующая терминология.

Место вызова
См. прямое место вызова и косвенное место вызова ниже.
Закрытая функция
Функция, тело которой не имеет свободных переменных, кроме параметров и тех, которые связаны другими функциями в рамках одного (возможно, взаимно рекурсивного) объявления.
Закрытие
Представление функции во время выполнения. Это включает указатели на код функции вместе со значениями любых переменных, используемых в теле функции, но фактически определённых вне функции, в области видимости. Значения таких переменных, в совокупности известные как окружение, необходимы, так как функция может быть вызвана из места, где исходные связи таких переменных больше не находятся в области видимости. Группа, возможно, взаимно рекурсивных функций, определённых с помощью let rec, совместно используют одно закрытие. (Примечание для разработчиков: в исходном коде Flambda закрытие всегда соответствует одной функции; набор закрытий относится к группе таких.)
Переменная закрытия
Член окружения, хранящийся в закрытии данной функции.
Константа
Некоторая сущность (обычно выражение), значение которой известно компилятору на этапе компиляции. Константость может быть явной из исходного кода или выведена оптимизаторами Flambda.
Закрытие константы
Закрытие, которое статически выделяется в объектном файле. Почти всегда часть окружения такого закрытия пуста.
Определяющее выражение
Выражение e в let x = e in e’.
Прямое место вызова
Место в коде программы, где вызывается функция, и известно на этапе компиляции, какая функция всегда будет вызываться.
Косвенное место вызова
Место в коде программы, где вызывается функция, но не является прямым местом вызова.
Программа
Совокупность связей символов, формирующих определение единицы компиляции (т.е. файла .cmx).
Специализированный аргумент
Аргумент функции, который, как известно, всегда хранит определённое значение во время выполнения. Они вводятся инлайнером при специализации рекурсивных функций; и проходом unbox-closures. (См. раздел 23.4.)
Символ
Имя, ссылающееся на определённое место в объектном файле или исполняемом изображении. В этом месте будет находиться некоторое константное значение. Символы можно исследовать с помощью инструментов, специфичных для операционной системы (например, objdump в Linux).
Связь символа
Аналогично выражению let, но работающему на уровне символов, определённых в объектном файле. Адрес символа фиксирован, но он может быть связан как с константными, так и с неконстантными выражениями.
Верхний уровень
Выражение в текущей программе, которое не заключено внутри любого объявления функции.
Переменная
Именуемая сущность, которой некоторое значение OCaml связывается выражением let, конструкцией сопоставления с образцом или аналогичным образом.

© 1995-2022 INRIA.
https://v2.ocaml.org/releases/5.0/htmlman/flambda.html

Spec-Zone.ru

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