-
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
Вопиющий случай распухания кода #362
Comments
Данный пример содержит открытые e-переменные. Есть другой суперкомпилятор (помимо кривого ограниченного оптимизатора в Рефале-5λ), который умеет обрабатывать программы с открытыми e-переменными — MSCP-A. Интересно посмотреть на нём этот пример. Версия суперкомпилятора — последний коммит TonitaN/MSCP-A@bf4a517 на момент написания этого комментария. Пример
успешно отсуперкомпилировался, получилась такая программа: Остаточная программа
Скачать: rsd_bloat-basic.ref. Видно, что получилось 11 базисных конфигураций. Так что можно ожидать, что при использовании перестройки сверху получится около десятка экземпляров, а не 56, как сейчас. Уже неплохо. Перестройка снизу часто строит экземпляры, соответствующие развёрнутым виткам цикла. Эти витки затем подставляются на этапе прогонки экземпляров и тем самым раздувают программу. Перестройка сверху избегает развёртки витков (по сравнению с перестройкой снизу), на этапе прогонки экземпляров встраиваний должно быть меньше. |
imho перестройка сверху точно нужна: здесь явный комбинаторный взрыв. Какая именно из двух --- вот не знаю. |
Почему у меня рефальские исходники, упомянутые в этой заявке, качаются с дополнительным расширением txt? Баг или фича? |
ВыводБуду делать халтурную перестройку сверху. @TonitaN, спасибо! |
Да не за что, ты же и сам пришел к тому же выводу. Я повтыкала в конфигурации Loop-а, и всё 🤓. |
Ну, асилила многабукаф. Ну, и ты могла прийти и к другому выводу. Кстати, поведение MSCP-A на этом примере ожидаемо? Вот этот комментарий #332 (comment) пыталась читать? |
Да, поведение ожидаемо. Комментарий не читала, кстати, неочевидные утверждения из него пока что мне не очевидны. Для дерева (бесконечного) они, понятное дело, верны. А вот что касается графа --- сходу нет интуиции, что при перестройке снизу одна из конфигураций после обобщения не может обобщиться с какой-то из тех, которые находятся на отрезке от верхней (не обобщённой) до нижней конфигурации. Тогда уже может пойти развитие совсем другой истории. Да, это оффтопик, надо было написать ответ к тому комментарию. |
А я ждал комментарий там 😊. |
Ты лучше туда скопируй комментарий, я на него отвечу. |
• Устранение дублирования кода. • Прояснение имён переменных.
Рефакторингом в чистом виде он, скорее всего, не является, т.к. может изменить имена переменных в экземплярах, наблюдаемые в логе.
Содержимое лога для теста bloat-basic.ref (приложенного к заявке) не изменилось за исключением вывода самой истории. Т.е. на данный тест замена никак не повлияла. На других примерах работу не испытывал.
Фактически, развёрнут последний виток цикла.
• Удалено промежуточное звено SpecCall-BuildSignature-***, • функция SpecCall-CheckSignature выпала из цепочки и стала нерекурсивной (избыточные параметры удалены).
Экземпляры строятся как обычно, но при обнаружении зацикливания более ранний экземпляр в истории также подменяется вызовом обобщённого вроде Loop@3 { (e.0) t.1 t.2 e.3 = <Loop@8 (e.0) t.1 W S t.2 e.3>; }
Перемена нумерации позволяет тривиальным сигнатурам на дне истории (сигнатурам псевдоэкземпляров Func@0) выглядеть в логеконсистентно с последующими сигнатурамии Func@n, где нумерация начинается с 0.
Два автотеста недописаны (нет функции Go) и поэтому выключены (переименованы в *._ref). Случайно сгенерированный автотест, заваливший CI, сохранён и на нём компилятор по-прежнему падает. Смысл коммита — не потерять эти тесты. Когда проблема #362 будет исправлена, тесты opt-tree-bloat-*._ref будут дописаны и включены.
Предыдущие коммиты проблему не исправилиШаг в правильном направлении был сделан, но этого оказалось мало. «Халтурная» перестройка сверху, с одной стороны, уменьшила размер лога bloat-basic.log, теперь он весит 6,3 Мбайт (ранее 11 Мбайт), и уменьшила размер остаточной программы (residual.txt) до 6 483 строк (ранее 15 555). С другой этого оказалось недостаточно для того, чтобы автотесты стали проходить (см. коммит перед этим комментарием). В процессе «халтурных» обобщений ранее специализированные экземпляры подменяются на вызов более позднего экземпляра:
Затем, на проходе специализации экземпляров эти функции тривиально встраиваются. (Заметим, что теперь специализация может увеличивать число шагов рефал-машины). Собственно, поэтому она и халтурная. Таких «перестроенных» экземпляров в остаточной программе (до прогонки экземпляров) оказалось 24 (из 61 экземпляра всего). Что делать дальше?Пока не знаю. Отношение остановки SCP4Возможный вариант — ослабить отношение остановки, приблизив его к «упрощающему отношению» SCP4: Отношение остановки SCP4 позволяет взаимозаменять s- и t-параметры с символами, а также игнорирует все e-параметры в исторической конфигурации (следует из правила «если
Здесь бы В актуальной реализации для ускорения проверки условия остановки отношение Хигмана-Крускала предваряется отношением Хигмана, т.е. отношением подпоследовательностей на цепочках токенов. Отношение Хигмана проверяется быстро, за линейное время O(|s| + |t|), поэтому если оно не выполняется, то заведомо более дорогое отношение Хигмана-Крускала (O(|s|×|t|/log(|t|) + |t|×log(|t|)), см. статью) можно не вычислять (т.к. второе влечёт первое). (Оптимизация не умозрительная, ускорение проверки отношения было очевидно по профилю.) Следовательно, при ослаблении отношения остановки на выражениях нужно предусмотреть адекватное ослабление отношения на цепочках токенов. Должен сохраняться инвариант: отношение на выражениях влечёт отношение на строках токенов записи этих выражений. Более грамотная чистка от неиспользуемых функций (см. #228)Функции вида
остаются в синтаксическом дереве и затем на этапе прогонки экземпляров оптимизируются. Хотя они могут оказаться неиспользуемыми и после чистки в финальное дерево не попадут (конкретно Так что оптимизация (в частности прогонка) на последнем этапе должна быть ленивой — не должна оптимизировать те функции, которые ни разу не вызываются. Либо выделить вспомогательный проход прогонки только перестроенных функций. Эти функции заведомо прогоняются (по построению форматы аргументов экземпляров всегда правильные), и будут прогоняться даже при поддержке активных аргументов (#230), такой проход может заранее очистить дерево от ненужного мусора. И этот проход можно даже включать для режима Настало время удивительных историйПопробовал я оптимизировать остаточную программу rsd_bloat-basic.ref, построенную суперкомпилятором MSCP-A. Гипотеза: если мы уже имеем оптимизированную программу, в которой выделены базисные конфигурации, то повторная суперкомпиляция должна дать тривиальный результат. Но оказалось всё интереснее. Компилятор начал создавать экземпляры для функций-базисных конфигураций, это в целом ожидаемо. Интересное поджидало в логе. Если я правильно понимаю суперкомпиляцию, то в полностью факторизованном графе в историях конфигураций не должно быть пар, связанных отношением остановки. Т.е. если история конфигурации @TonitaN, я правильно понимаю суперкомпиляцию? А вот что у меня получилось в логе:
Куча экземпляров имеют неправильные истории. Например, если в последней сигнатуре для Тут нужно пояснить, что такое Специализация может зациклиться. Пусть в функции Чтобы иметь возможность прерывать цепочки специализаций, для каждого экземпляра формируется история. Если экземпляр Особый случай — когда специализируются вызовы внутри самой специализируемой функции. Оказалось, что в этом случае в самой функции (если у неё в формате есть переменные-аккумуляторы) начинают формироваться совершенно бестолковые экземпляры, соответствующие развёртыванию одного витка цикла. Пришлось для таких случаев искусственно формировать историю, на дне которой лежит тривиальная сигнатура — сигнатура, построенная как обобщение образцов всех предложений. Такой сигнатуре приписывалось имя несуществующего экземпляра В старом коде (до реализации «халтурной» перестройки сверху) при зацикливании между текущим вызовом и сигнатурой для В новом коде получилась вот такая фигня:
Как же она получилась? Экземпляр
История этого экземпляра:
При специализации первого вызова формируется история:
Для двух последних строчек срабатывает отношение Хигмана-Крускала (действительно,
Но для новой истории
никаких проверок не выполняется, и получается такая фигня. Правильнее для С другой стороны, получается, что если мы при суперкомпиляции Рефала делаем перестройку сверху и вычисляем обобщённую сигнатуру, для этой сигнатуры нужно опять проверить отношение остановки, т.к. оно может сработать и для обобщения. Т.е. получается, что если у нас была история вида
причём
может оказаться Например, была история
Для
и тут условие остановки для @TonitaN, ты знала об этом? Возможно, расширение отношения Хигмана-Крускала в SCP4 обусловлено желанием сделать условие остановки более монотонным, т.е. избегать такого каскада перестроек. Действительно, по упрощающему отношению SCP4 можно в нижней конфигурации не только стирать элементы, но и заменять произвольные участки на e-параметры. В И по упрощающему отношению SCP4 в истории
сигнатура экземпляра К слову, для исходного теста Что делать?Если делать нормальную перестройку сверху, как описано в #332 (comment), то там подобный каскад обобщений будет получаться совершенно естественно. В актуальной реализации можно в проверку отношения остановки добавить вычисление обобщения + цикл. |
@Mazdaywik , кстати, нигде не видела такого требования к графу суперкомпиляции, чтобы на его путях не было выполнено |
Определение. Let-экземпляр — экземпляр вида
При халтурной перестройке сверху старый экземпляр становится Let-экземпляром. Предлагается следующий вариант алгоритма специализации:
Во-первых, данный алгоритм решит вторую проблему из моего предыдущего комментария — будет работать каскад обобщений. Во-вторых, код должен немного упроститься — мы сразу построим вызов функции для данной сигнатуры и дальше уже таскать подстановку не нужно будет. Недостаток алгоритма — лишние let-экземпляры. Эти экземпляры можно устранять отдельным проходом прогонки, выполняемым после цикла специализации. Он будет выполняться всегда, даже если ключ А для решения первой проблемы нужно ослаблять условие остановки. |
Для специализации это очевидный рефакторинг, т.к. разметка выполняется не более чем один раз в любом режиме компиляции. (Вообще, этот код давно надо было упростить.) Для прогонки это не рефакторинг, т.к. при прогонке экземпляров содержимое лога будет немного другим. Помимо непосредственного удаления кода «обновления» структур данных, были сделаны некоторые простые рефакторинги в окружающем коде.
Ранее было обнаружено, что с новым алгоритмом специализации компилятор в некоторых тестах увязает, и был добавлен грязный хак, ограничивающий максимальное число экземпляров (54c28ca, #332). Предыдущие два коммита стали строить дополнительные let-экземпляры, из-за чего порог стал достигаться раньше и глубина оптимизации снизилась. Поэтому потребовалось поднять порог. Порог был поднят до 150 по двум причинам: • В тесте saved-test-10_Mon-Aug-23-21-51-16-UTC-2021.ref (81667ba) на пороге в 100 экземпляров условие остановки не срабатывает (до let-экземпляров срабатывало). Опытным путём было выяснено, что условие установки срабатывает на пороге между 140 и 145, было выбрано круглое значение. Заметим, что этот тест в текущем коммите проходит. • Тест saved-test-77_14.08.2021_20-00-01,67-big.ref (41ce59e, #314), напротив, проходит только благодаря наличию этого лимита (на момент написания коммита 41ce59e не анализировалась проблема с непрохождением теста, просто был повышен порог). С лимитом 200 тест не проходит, с лимитом 150 проходит.
Конкретная проблема предыдущего коммита была в том, что на вход финальной прогонки экземпляров подавалось дерево с функцией Func*n, но при этом функция Func в дереве могла отсутствовать. Для прогонщика это было нарушением инварианта. Исправление свелось к тому, что Func*n уже больше не подлежат финальной прогонке, т.к. подлежат только экземпляры — функции с другим суффиксом @n. Была и другая проблема (не проявлялась). Она была в том, что ранее на финальном этапе выполнялась разметка всех функций для прогонки, а функции с именами вида Func*n прогонялись уже не адекватно, т.к. предложения в них уже могли быть размножены, и прогонка одного предложения приводила бы к неправильной функции Func*‹n+1›.
Обнаружилась проблема, связанная с исчерпанием числа шагов на тесте varcopy-fail.ref. Я предположил, что проблема в лишних let-экземплярах, которые съедают один шаг. Поэтому всё-таки реализовал #230 в рамках исправления #362. Как оказалось, реализовать поддержку активных вызовов оказалось проще, чем я думал ранее.
Проблему прекрасно описывает обширный комментарий в коде. Тест varcopy-fail.ref переименован, т.к. он выявлял проблему, решённую в коде.
Промежуточные выводыРеализована «халтурная» перестройка сверху. Let-экземпляры теперь встраиваются, после встраивания let-экземпляров программа чистится от избыточных функций. Некоторые выводы. Тест
|
Int-Command { | |
(e.Defines) (e.Command) s.Cycles (e.AST) | |
= e.Command | |
: { | |
pass e.Code | |
= s.Cycles | |
: { | |
0 = 0 e.AST; | |
s._ | |
= <Int-Code (e.Defines) e.Code s.Cycles (e.AST)> : s.Cycles^ e.AST^ | |
= <Dec s.Cycles> e.AST; | |
}; | |
if t.Cond t.Then t.Else | |
= <Int-Cond t.Cond> | |
: { | |
True = <Int-Command (e.Defines) t.Then s.Cycles (e.AST)>; | |
False = <Int-Command (e.Defines) t.Else s.Cycles (e.AST)>; | |
}; | |
when t.Cond e.Then /* нет else */ | |
= <Int-Cond t.Cond> | |
: { | |
True = <Int-Code (e.Defines) e.Then s.Cycles (e.AST)>; | |
False = s.Cycles e.AST; | |
}; | |
begin e.Code | |
= <Int-Code (e.Defines) e.Code s.Cycles (e.AST)>; | |
call s.Func e.Args | |
= s.Cycles <s.Func e.Args e.AST>; | |
trace e.Message | |
= <Log-AST ('Pass ' <Symb s.Cycles> ' (' e.Message ')') e.AST> : e.AST^ | |
= s.Cycles e.AST; | |
loop-for-warm-functions e.Code | |
= <Int-LoopForWarmFunctions (e.Defines) e.Code s.Cycles (e.AST)>; | |
s.Proc | |
= <Int-Lookup s.Proc e.Defines> : e.Code | |
= <Int-Code (e.Defines) e.Code s.Cycles (e.AST)>; | |
} | |
} |
Другой пример — в записке @Kaelena: РПЗ_Калинина_Автоматическая разметка оптимизируемых_функций_2020.pdf.
В результате получалось множество экземпляров, большинство из которых были транзитными — их прогонка не требовала сужений. А в случае простых интерпретаторов, нетранзитные тоже имело смысл прогонять.
Сейчас такой потребности нет — интерпретатор можно схлопнуть в одну функцию, пометить её *$OPT
и её вызовы будут либо прогоняться, либо специализироваться. Ну, в теории, я пока не пробовал на самом деле. А значит, лишних экземпляров быть не должно или их должно быть гораздо меньше.
Так что, возможно, стоит отказаться от этого прохода, или хотя бы внимательно проанализировать её плюсы и минусы.
Проблема
При проверке pull request’а #361 упали автотесты: https://github.com/bmstu-iu9/refal-5-lambda/pull/361/checks?check_run_id=3404836202.
Из артефактов стало понятно, что не хватило лимита шагов на случайно сгенерированный тест:
Сам тест.
Данный тест удалось сузить до следующей программы (которой по-прежнему не хватает шагов):
Скачать: bloat-condition.ref. Последнее условие в первом предложении пустое, и оно нужно. Без него ошибка не воспроизводится.
Я достал из лога программу с рассахаренными условиями (т.к. выпало на тесте без
-OC+
), упростил полученную программу, переименовал функции и переменные:Скачать: bloat-basic.ref. Заметим, что функция
Loop
является синтаксическим мономом, если первое предложение убрать, её поведение сохранится.Полный лог компиляции (11 мегабайт): bloat-basic.log.
Так вот, для функции
Loop
строится 56 экземпляров:Фрагмент лога
Остаточная программа до прогонки экземпляров содержит 835 строк, после прогонки экземпляров — 15 555 строк (да, четыре пятёрки: residual.txt).
Типичный экземпляр до прогонки экземпляров:
Начало финальной остаточной программы:
Почему вопиющий случай? Зачем новая заявка (почему не #332)?
Вопиющая потому что
Заявка #332 предполагается как долгоиграющая — когда я её буду исправлять, не известно. А эту хочется исправить в самое ближайшее время.
Возможные решения
Забить
Т.е. запушить новый коммит (он должен быть с обновлением
NEWS.md
, и, если повезёт, в случайных тестах эта ошибка не повторится.Придумать костыль
Например, снизить наибольшее число экземпляров (сейчас такой костыль есть, ограничивает число экземпляров 99, 54c28ca). Либо можно ограничить максимальную глубину истории. Для последнего можно самоприменить компилятор и посмотреть максимальную глубину истории в нём.
Ослабить отношение остановки
Сейчас используется честное отношение Хигмана-Крускала. Выбор другого варианта — это всё-таки исследовательская работа, быстро сделать не получится. Идеи описаны в комментариях #332 (comment), #332 (comment) и #332 (comment).
Специальное сопоставление с образцом для специализации
Описано оно в комментарии #332 (comment). Если кратко, то алгоритм сопоставления с образцом, злоупотребляя динамическим обобщением, гарантирует, что для любого предложения специализируемой функции построится не более одного предложения в экземпляре. Участки аргумента, сопоставление с которыми приводит к ветвлению, принудительно обобщаются.
e.X … e.Y
, описанных в том же комментарии Древесные оптимизации раздувают программы #332 (comment) и реализованных в фиксации 916efbf.Перестройка сверху (честная)
В комментарии #332 (comment) предлагается серьёзно модифицировать алгоритм оптимизации, чтобы при специализации использовалась перестройка сверху. Скорее всего, это снизит число экземпляров в данной задаче до разумной величины (см. первый комментарий #362 (comment)).
Loop@10
иLoop@26
различаются только s- и t-переменными:Перестройка сверху (халтурная)
Сейчас при обнаружении зацикливания между старой сигнатурой
F@i
и новой сигнатуройF@j
вычисляется их обобщение, назовём егоF@k = MSG(F@i, F@j)
. ЕслиF@k ≡ F@i
, то это обычное зацикливание, вызываем ранее построенный экземпляр. Если нет, то вызывается и строится экземпляр для обобщённой сигнатурыF@k
. История экземпляраF@k
растёт от историиF@j
. Это перестройка снизу.Предлагается сделать почти грязный хак. А именно, при срабатывании условия остановки в случае
F@k = MSG(F@i, F@j) ≠ F@i
строить не только экземплярF@k
, как и раньше, но ещё и патчить старый экземплярF@i
, заменяя всё его тело на единственный вызовF@k
:История экземпляра
F@k
будет расти не от историиF@j
, а отF@i
. Назовём это халтурной перестройкой сверху.Очевидно, что экземпляры, построенные после
F@i
, в программе останутся, хоть вызываться изF@i
не будут. Возможно, специализатор продолжит оптимизировать в них заведомо бессмысленные вызовы. Они могут потом потереться на последнем проходе — удалении невызываемых функций¹. Или не потереться, если в них есть «косые стрелки».¹ Этот проход в качестве корневого множества рассматривает все функции без суффиксов — функции исходной программы. Поэтому в логе можно видеть функцию
Loop
, которая ниоткуда не вызывается.Пропатченные экземпляры вида
F@i
из примера выше будут встраиваться в точки вызова на проходе прогонки экземпляров.Если специализация разных дочерних экземпляров будет требовать разный патч для родителя, то будет выбираться произвольный патч — какой первый попадётся.
-OA
будут встраиваться в точки вызова, а затем вытираться как неиспользуемые).Вывод
А нет пока вывода. Я ещё не выбрал вариант. Напишу в комментариях. Склоняюсь к последнему.
@TonitaN, какой вариант тебе больше нравится?
The text was updated successfully, but these errors were encountered: