Skip to content
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

Удалять и не порождать неиспользуемые функции #228

Open
Mazdaywik opened this issue Jun 30, 2019 · 9 comments
Assignees
Labels
Milestone

Comments

@Mazdaywik
Copy link
Member

Mazdaywik commented Jun 30, 2019

UPD: более верная постановка задачи в комментариях (#228 (comment)).

Эта задача — подзадача для #91.

@InfiniteDisorder реализовал оптимизацию встраивания и прогонки #122, однако, реализовал не оптимально. Когда в процессе прогонки в программе формируется вызов «усечённой» функции <Func*n …>, тело функции Func*n добавляется к синтаксическому дереву. Добавление это преждевременное, поскольку на следующей итерации оптимизации вызов Func*n может прогнаться/встроиться и в конечной программе Func*n никогда вызываться не будет.

Правильным является вариант, когда функции Func*n добавляются в дерево только тогда, когда вызов <Func*n …> становится холодным, т.е. не подлежащим дальнейшей оптимизации. Но что, если лимит итераций иссяк, но функцию Func*n можно ещё прогнать? В этом случае усечённая Func*n должна добавляться в OptTree-Drive-Finalize.

Вообще, привлекательно выглядит вариант вообще не добавлять в дерево тела встраиваемых или прогоняемых функций, если они не вызываются из единицы трансляции или не entry. Но так делать нельзя, поскольку такие функции могут вызываться через Mu, т.е. должны быть доступны для рефлексии. Функции Func*n или Func@n могут пристутствовать в программе или не присутствовать, в зависимости от ключей компиляции, но функция Func без суффиксов присутствовать должна обязательно. В принципе, можно не добавлять в дерево вспомогательные функции вроде Func\n, Func=n, Func:n и другие (например, Func?m?n, добавляемые Desugaring-UnCondition), если они растворились при оптимизации. Надо оценить затратность реализации и, возможно, так и сделать.

@Mazdaywik Mazdaywik added the task label Jun 30, 2019
@Mazdaywik Mazdaywik self-assigned this Jun 30, 2019
@Mazdaywik Mazdaywik changed the title Оптимизация оптимизации прогонки Отложенное создание функций Func*n при прогонке Jul 4, 2019
@Mazdaywik
Copy link
Member Author

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

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

Mazdaywik added a commit that referenced this issue Aug 1, 2019
Теперь, во-первых, создаётся меньше функций Func*n, поэтому коммит
частично решает задачу #228, во-вторых, компилятор работает, в частности,
самоприменяется быстрее, поэтому ссылка на #239.
@Mazdaywik Mazdaywik changed the title Отложенное создание функций Func*n при прогонке Удалять неиспользуемые функции с суффиксами Aug 1, 2019
@Mazdaywik
Copy link
Member Author

Тема задачи уточнилась. Здесь теперь предлагается после выполнения оптимизаций чистить программу от неиспользуемых функций с суффиксами. Чистить от любых неиспользуемых локальных функций нельзя, ибо есть функция Mu, выполняющая вызов по имени. А функции с суффиксами вызывать через Mu не следует.

При этом программа также может очиститься от функций \n, =n и :n, что не страшно. Если вложенные функции, присваивания и блоки встроились/прогнались, значит, вспомогательные функции уже не нужны.

@Mazdaywik
Copy link
Member Author

Задача #254 (новая реализация метафункций) ввела понятие метатаблиц — особого рода функций, содержащих отображения имён на все функции без суффиксов текущей области видимости.

Поэтому теперь неиспользуемые функции определяются синтаксически — все entry-функции считаем используемыми, все функции с именами INIT и FINAL считаем используемыми, все функции, которые прямо или косвенно из них доступны — тоже используемые. Метафункции содержат указатели на метатаблицы, поэтому если метафункции используются, то автоматически используются и все локальные функции. Если метафункции не используются, то можно удалять и неиспользуемые локальные функции независимо от того, есть у них суффиксы или нет.

@Mazdaywik Mazdaywik changed the title Удалять неиспользуемые функции с суффиксами Удалять неиспользуемые функции Jun 24, 2020
@Mazdaywik Mazdaywik removed their assignment Oct 4, 2020
@Mazdaywik Mazdaywik added study and removed task labels Oct 4, 2020
@Mazdaywik Mazdaywik added this to the study fall 2020 milestone Oct 4, 2020
@Mazdaywik
Copy link
Member Author

Mazdaywik commented Oct 4, 2020

Помечу как курсовую, хотя она мне кажется слишком простой для курсовой.

Берите, пока я не передумал.

@Mazdaywik Mazdaywik added task and removed study labels Oct 9, 2020
@Mazdaywik Mazdaywik removed this from the study fall 2020 milestone Oct 9, 2020
@Mazdaywik
Copy link
Member Author

Не, она не подходит на курсовую. Бессодержательная.

@Mazdaywik
Copy link
Member Author

Mazdaywik commented Oct 22, 2020

Древесная оптимизация может сделать используемые функции неиспользуемыми. В исходной программе функция E была entry и вызывала функцию L. Оптимизатор или встроил L в точку вызова, или породил новый экземпляр L@1. Функция L не вызывается.

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

Поэтому предлагается одновременно оптимизировать функции и помечать среди них используемые. Как это сделать?

Рассмотрим общий случай включённой прогонки и специализации. Специализация на данный момент имеет оптимизацию: после анализа функции (шагов прогонки вызовов в нём и прохода специализатора) она помечается «замороженной» и на последующих проходах не сканируется.

Предлагается сделать зеркальный шаг: «замораживать» (другой заморозкой — блокировать) также функции, которые ещё никем не вызваны. В начале заблокированы только входные точки: entry, INIT, FINAL. Проходы прогонки и специализации над ними порождают функции-хвосты F*n и экземпляры F@n. Перед заморозкой этих функций их тела сканируются и извлекаются вызываемые из них функции (включая указатели и АТД). Функции с данными именами разблокируются. Следующие проходы прогонки и специализации анализируют хвосты, экземпляры и разблокированные функции. Повторяем до тех пор, пока у проходов есть работа — если новый проход не породил хвосты, экземпляры и ничего нового не разблокировал, оптимизация завершается.

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

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

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

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

Заметка на полях

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

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

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

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

Заметим ещё, что вызов неиспользуемой функции может появиться и в результате прогонки. Рассмотрим пример:

$ENTRY F {
  s.F = <Test s.F> <s.F>;
}

Test {
  &NotUsed = 1;
  s._ = 2;
}

NotUsed {
  = "Hello!";
}

Указатель на NotUsed не может появиться в поле зрения. Но при прогонке может появиться вызов этой функции:

$ENTRY F {
  &NotUsed = 1 <NotUsed>;
  s.F = 2 <s.F>;
}

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

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

@Mazdaywik
Copy link
Member Author

Интересный нюанс. Допустим, у нас есть программа

$ENTRY Test {
  e.X = <Map {{ &Test\1 … }} e.X>;
}

Test\1 { … }

Map {  …  }

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

Но! Получится вот что:

$ENTRY Test {
  e.X = <Map@1 e.X>;
}

Test\1 { … }

Map {  …  }

Map@0 { }

Map@1 {
  …

  e.dyn = <Map@0 {{ &Test\1 … }} e.dyn>
}

Из-за аварийного предложения функция Test\1 формально продолжает использоваться! Но она в программе явно не нужна! Возможное решение — преобразовывать все упоминания функций в аварийном предложении на упоминания пустых функций

  e.dyn = <Map@0 {{ &Test\1@0 … }} e.dyn>

Поскольку заранее неизвестно (вернее, заранее трудно сказать), по чему будут специализированы функции, @0-функции нужно будет добавлять всем функциям в единице трансляции. Всё равно у нас есть средство чистки, которое удалит лишние.

Но что делать в этой ситуации?

$EXTERN Ext;

$ENTRY Test {
  e.X = <Map &Ext e.X>;
}

Map { … }

В аварийном предложении будет

  e.dyn = <Map@0 &Ext e.dyn>

Если её переименовывать в &Ext@0, то, значит, @0-функции нужно добавлять и для внешних функций. Либо, при переименовании проверять, что функция является локальной.


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

Mazdaywik added a commit that referenced this issue Nov 29, 2020
Ранее это не приводило к проблемам, т.к. ни прогонка не осуществляется
в функциях с условиями, не сами функции с условиями не подлежат прогонке.
Mazdaywik added a commit that referenced this issue Nov 29, 2020
Поддержка была реализована на основе графа вызовов функций, используемом
для авторазметки (#252). Реализованный алгоритм обхода графа не только
выявлял функции, пригодные и не пригодные для прогонки, но и функции
недостижимые. Этим и воспользовались.

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

Корнями графа при обходе считаются все функции без суффиксов — так проще
не в ущерб корректности.

При построении в граф также добавляются теги АТД-скобок как указатели.
На корректность разметки для прогонки это никак не влияет, поскольку
теги абстрактных скобок обычно являются пустыми функциями. Но они нужны
алгоритму чистки, иначе определения тегов будут удалены и программа станет
некорректной.
Mazdaywik added a commit that referenced this issue Nov 29, 2020
Рассахариватель почему-то считал их определениями и стирал $EXTERN’ы
с соответсвующими именами.
Mazdaywik added a commit that referenced this issue Nov 29, 2020
Теперь не только удаляются $EXTERN’ы, соответствующие определённым функциям,
но и удаляются повторяющиеся $EXTERN’ы.
Mazdaywik added a commit that referenced this issue Nov 29, 2020
Решение одной из проблем, описанных в

#228 (comment)

Теперь пустая функция Func@0 создаётся для каждой функции и каждого
extern’а. А в аварийных предложениях заменяются ссылки на функции ссылками
на их пустые сёстры.
@Mazdaywik
Copy link
Member Author

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

@Mazdaywik Mazdaywik changed the title Удалять неиспользуемые функции Удалять и не порождать неиспользуемые функции Dec 8, 2020
@Mazdaywik Mazdaywik self-assigned this Jan 11, 2021
@Mazdaywik Mazdaywik added this to the 3.3 milestone Mar 11, 2021
@Mazdaywik
Copy link
Member Author

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

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

(ColdFunction s.ColdBy s.Scope (e.Name) e.Body)

Причины на данный момент две: DRIVE и SPEC. Предлагается добавить следующие: UNUSED и SCANNED.

  1. В начале работы программы все функции, кроме входных точек ($ENTRY, __INIT, __FINAL), заморожены как UNUSED.
  2. Проход прогонки оптимизирует размороженные функции. При этом хвостовые функции он не добавляет.
  3. Размораживаем функции DRIVE.
  4. Проход специализации оптимизирует размороженные функции.
  5. Размораживаем функции SPEC.
  6. Проход разморозки проходит по всем размороженным функциям, выбирает из них ссылки, функции замораживает как USED. Для найденных ссылок размораживаются функции UNUSED, генерируются функции-хвосты (если они ещё не сгенерированы).
  7. Если есть размороженные функции и остались проходы оптимизации, иди к пункту 2. Если есть размороженные функции, но проходы оптимизации закончились, иди к пункту 6.

В данной схеме пункты 2–3 и 4–5 могут отсутствовать, если соответствующие оптимизации выключены.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant