-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Автоматическая разметка оптимизируемых функций #252
Comments
Прекрасная задача для исследования в рамках образовательного процесса! В рамках курсовой работы нужно будет научиться распознавать рекурсивные, взаимно-рекурсивные и нерекурсивные функции и на основе этого развешивать метки На ВКР можно будет вынести всё остальное. Почему так? Потому что все три разметки ( Поэтому построение графа вызовов (или иной способ определения «рекурсивности») и непосредственно из него следующую разметку |
Задача не была выбрана в качестве курсовой. |
Пока они только распознаются компилятором и протягиваются до OptTree.ref и OptTree-Drive.ref. Никак не используются. Их нужно было добавить до обновления стабильной версии, чтобы та их принимала и игнорировала.
Коммит труден для восприятия в виде diff’а, лучше смотреть «старую» и «новую» версии отдельно (в gitk есть соответствующий переключатель).
Некоторые важные соображения и уточненияУточним определения.
Цель работы: реализовать корректную и безопасную разметку, при этом избыточность допустима. С некорректной разметкой компилятор просто будет неработоспособен. С небезопасной — будет зацикливаться. На самом деле, оптимизатор всегда делает конечное число проходов — максимальное их количество задаётся параметром Избыточная приведёт лишь к бесполезным попыткам оптимизировать те вызовы, которые оптимизировать нельзя. Неизбыточная расстановка не возможна, поскольку проблема остановки алгоритмически неразрешима. Поэтому можно лишь эту избыточность снижать (в рамках ВКР этого не требуется в принципе). Как написано в предыдущих комментариях — расстановка меток Метки
|
• Сужения типов переменных (в замыканиях Map вместо e.… t.…). • Разбиение функции с двумя форматами на две.
Вместо шаблона специализации по ошибке строился список переменных из обобщения, переменным давались индексы .STAT#n и .dyn#n. Из-за того, что ранее индексы формировались неправильно (38523f9), специализация не выполнялась — вызов считался с тривиальной сигнатурой и ошибка не воспроизводилась.
При запуске автотестов выяснилось, что функции GetVariableMatches и GetVariablesSpecType неправильно помечают переменные из образца статическими. Эти две функции были полностью переписаны с нуля. Для прояснения диффа коммита вспомогательные функции, которые вызывались из первоначальных версий, удалены не были, они будут удалены следующим коммитом. Теперь всё работает.
По неочевидной причине @Kaelena запретила маркировать функции, помеченные как $ENTRY, для прогонке. Видимо, она что-то перепутала.
По непонятной причине @Kaelena запретила специализацию функций с любыми суффиксами, хотя достаточно только запрещать специализацию только экземпляров.
Поддержка была реализована на основе графа вызовов функций, используемом для авторазметки (#252). Реализованный алгоритм обхода графа не только выявлял функции, пригодные и не пригодные для прогонки, но и функции недостижимые. Этим и воспользовались. Граф строится по упрощённой схеме: не выполняются встраивания косвенных вызовов, метатаблиц и inline-функций. Всё это нужно для разметки прогоняемых вершин, но избыточно в рамках настоящей задачи. Корнями графа при обходе считаются все функции без суффиксов — так проще не в ущерб корректности. При построении в граф также добавляются теги АТД-скобок как указатели. На корректность разметки для прогонки это никак не влияет, поскольку теги абстрактных скобок обычно являются пустыми функциями. Но они нужны алгоритму чистки, иначе определения тегов будут удалены и программа станет некорректной.
Мотивация
В своём недавнем письме в рассылку я привёл пример того, как в Рефале-5λ можно осуществить первую проекцию Футамуры. В письме приведён пример интерпретатора стекового языка программирования и оптимизированная программа в псевдокоде. Программа содержит более трёх десятков специализированных функций, большинство из которых транзитные и могут быть встроены (inline).
Встроить их компилятор не может, поскольку они не помечены как встраиваемые. Пользователь не может задать атрибут
$INLINE
, поскольку функции создаются компилятором. Таким образом, при оптимизации могут создаваться функции, которые можно снова оптимизировать.Чтобы их оптимизировать, компилятор должен обойти построенную программу, подходящим функциям назначить атрибуты оптимизации (
$DRIVE
,$INLINE
,$SPEC
) и запустить оптимизацию ещё раз.Может показаться, что пример с первой проекцией искусственный. Но есть и «естественные» примеры. Например, функцию
DoMapAccum-Aux@N
во многих случаях можно встроить. Рассмотрим пример: функцию отделяющую овец от козлов:Преобразованная программа будет иметь вид:
Очевидно, в первых двух предложениях
DoMapAccum@1
вызовDoMapAccum-Aux@1
можно встроить.Кроме того, компилятор может транслировать программы, в которых в принципе нет разметки для оптимизации (например, SCP4 или MSCP-A). Но функции, пригодные для оптимизации, в них могут быть.
Задача
Задача состоит в том, чтобы добавить в оптимизатор автоматическую разметку оптимизируемых функций и дополнительный внешний цикл оптимизации. На данный момент цикл только один: выполнять проходы
Simple
,Drive
иSpec
до неподвижной точки или до исчерпания счётчика. Предлагается добавить внешний цикл, который до неподвижной точки или исчерпания счётчика добавляет разметку функций, чистит программу от мусора (#228) и выполняет внутренний цикл. Причём счётчик должен быть сквозным.Самое интересное здесь — по каким критериям будет выполняться разметка. Разметка должна быть:
Понятно, что точная разметка представляет собой алгоритмически неразрешимую задачу, поэтому здесь нужны эвристики — критерии, хорошо работающие на практике,
Добавление меток
$DRIVE
Это, как мне кажется, одно из самых простых и очевидных.
Сначала строится граф вызовов функций — вершины помечены именами функции, в графе есть ребро от функции
F
до функцииG
, если в теле функцииF
есть хотя бы один вызовG
. Для рекурсивных функций в графе получаются петли.Компоненты сильной связности в графе будут соответствовать рекурсивным и взаимно-рекурсивным функциям. Функцию следует пометить как
$DRIVE
, если онаИз общих соображений такая разметка будет безопасной. Нерекурсивные функции прогонять безопасно. Взаимно-рекурсивная функция, которая вызывается в программе только один раз, при прогонке просто сожмёт эту компоненту сильной связности на одно звено.
Наверное, можно подобрать и другие простые признаки прогоняемых функций. Но этих, как мне кажется, на практике достаточно.
Добавление меток
$INLINE
Встраивание слабее прогонки, и поэтому, как ни парадоксально, признаки встраиваемых функций сложнее признаков прогоняемых.
Вообще, каким функциям стоит вешать метку
$INLINE
? Во-первых, тем функциям, чьи вызовы могут быть вычислены во время компиляции. Во-вторых, тем функциям, которым нельзя приписать метку$DRIVE
. Ведь если функцию можно прогонять, зачем себя ограничивать встраиванием?Когда функцию нельзя прогонять? Когда она рекурсивно разбирает свой аргумент, в рекурсивный вызов передаёт только часть аргумента. Например:
Eval
принимает единственный терм (её формат можно описать как<Eval t.Expr>
) и вызывает себя рекурсивно с частями этого терма.Apply
вызывает себя рекурсивно с частью первого терма аргумента.Соответственно, метку
$INLINE
нужно давать рекурсивным функциям, которые в один из своих аргументов передают часть этого же аргумента. Для этого нужен механизм вывода входного формата функции и дальнейшая проверка, как функция распоряжается аргументами. Вывод типов можно организовать простейший: ГСО от образцов.Что делать со взаимно-рекурсивными функциями, пока не понятно (upd: см. про
$SPEC
ниже). Можно ограничиться только непосредственной рекурсией.Встраивание функций, которые на каждом рекурсивном вызове уменьшают свой аргумент, безопасно. Поскольку рано или поздно этим аргументом окажется то, что невозможно будет разбить (переменная, символ или пустота) и встраивание остановится.
Заметим, что если будет реализована разметка неразменных аргументов (#231), то аргументы, значение которых уменьшается на каждом шаге рекурсии должны быть помечены как неразменные.
В сложных случаях может выделяться «лексикографический» порядок аргументов: либо первый уменьшается, а остальные меняются как угодно, либо первый не меняется, а уменьшается второй, а остальные меняются как угодно, либо первые два не меняются, уменьшается третий и т.д. На этот счёт предлагается не заморачиваться.
Добавление меток и шаблонов
$SPEC
Когда функцию разумно объявлять специализируемой? Когда она рекурсивна и один или несколько аргументов на рекурсивных вызовах не меняются. При этом желательно, чтобы фактическое значение константного аргумента было чем-то интересно и при его фиксации в теле функции можно было выполнить дополнительные преобразования. Но это уже неочевидная семантика. А неизменность аргумента на рекурсивном вызове — критерий синтаксический.
Для конкретных значений такого «константного» параметра можно создать различные экземпляры данной функции, где он будет «захардкожен». И предполагается, что такие вариации будут или эффективнее, или смогут быть прооптимизированы другими инструментами.
Таким образом, критерий специализации похож на критерий встраивания. Отличается он только тем, что интересующий аргумент не должен уменьшаться, а должен оставаться неизменным. В принципе, проверку на оба критерия можно объединить в одной процедуре.
Предложенный алгоритм не будет работать со взаимно-рекурсивными функциями, поэтому пару
DoMapAccum
+DoMapAccum-Aux
он не распознает. Возможно, стоит продумать расширение на такой случай.Для взаимно-рекурсивных функций можно строить что-то вроде дерева процессов. В корень поместить одну из функций, для каждого рекурсивного вызова построить рёбра, спускаться вниз, пока не встретим функцию из корня. В ней анализировать свойства аргументов. Если аргумент не изменил своего значения на всём пути — он статический для специализации. Если в аргумент дошла только часть его же самого — он неразменный для встраивания.
В корень по идее можно помещать всё, что угодно. Но разумнее помещать точку входа компоненты сильной связанности — функцию, которая вызывается извне. Если таковых несколько — одну из. Чисто из эстетических соображений.
Нюанс: функция встраиваемая и специализируемая одновременно
Алгоритм выше может пометить функцию и как встраиваемую, и как специализируемую одновременно. За примером далеко ходить не надо:
Первый аргумент константен, по нему можно специализировать. Второй аргумент уменьшается, по нему можно встраивать. (Действительно, почему бы не встроить этот вызов?
Он тогда сможет полностью просчитаться во время компиляции в
(A) (B) (C)
.)Что делать в случае такой разметки? Пытаться встроить, пока функция встраивается. Если не встраивается — пытаться проспециализировать. К слову, ответ на один из вопросов в #229.
Выводы
Детали, конечно, нужно уточнять. Но фронт работ нарисован довольно чётко.
The text was updated successfully, but these errors were encountered: