-
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
Древесные оптимизации нарушают семантику #276
Comments
Варианты с запретом на сравнение замыканий и нарушением аксиоматики отбрасываем. Рассмотрим приемлемые варианты. Научить прогонку и специализацию не нарушать семантикуТут возможны два пути: сужения и расширения. Путь сужения — не генерировать опасные конструкцииПроблема возникает, когда переменная с замыканием размножается в правой части прогоняемой или специализируемой функции. Следовательно, чтобы сохранять семантику, надо запрещать прогонку или встраивание в таких случаях. Нечто похожее уже делается — в прогонке запрещены сужения в замыкания, а в специализации построенные функции проверяются на «осмысленность». Цель одна и та же — не дать замыканиям просочиться в образец, что синтаксически некорректно. К сожалению, такой консервативный подход лишит возможности оптимизировать многие полезные функции, вроде, Путь расширения — усложнить логику для сохранения семантикиТребуется, чтобы при размножении в правых частях замыканий они всё равно считались равными. Значит, им нужно добавить некий «ключ», равенство которого соответствует семантическому равенству двух объектов. Правая часть вида
будет неявно преобразовываться к виду
Здесь
то правая часть преобразуется в
Конструкторы замыканий размножились, но разные их экземпляры имеют равные ключи, а значит, они будут считаться равными. Заметим, что в этом случае оптимизация сохраняет поведение и для «зеркального» случая: когда в разные моменты времени создаются два замыкания с одинаковым контекстом. Пример:
После рассахаривания получим:
После встраивания получим:
Ключи будут разные, а значит, объекты замыканий также будут не равны. Причём ключом может являться сам указатель на объект замыкания. Операция Сейчас Рефал-5λ поддерживает только безымянные вложенные функции. Предлагаемая реализация с разделением создания замыкания на две фазы позволяет реализовать и именованные взаимно-рекурсивные вложенные функции. Сравнивать замыкания по значениюСамый простой в реализации подход к решению этой проблемы. Минимальные правки рантайма. Недостаток — быстродействие. Но, с другой стороны, пока на Рефале-5λ никто не использует стиль программирования, требующий сравнения замыканий на равенство. Приравниваются обычно пассивные значения (или глобальные функции как метки АТД-скобок), функции обычно только вызываются. Отказаться от символов-замыканийПочему замыкания представляются s-переменными? Потому что Замыкание состоит из указателя на глобальную функцию и контекста в виде объектного выражения определённого формата. При этом глобальная функция ожидает именно такого формата от контекста. Если дать возможность пользователю манипулировать содержимым замыкания, то он может нарушить инварианты. Поэтому для скрытия реализации замыкания считаются s-переменными. В общем, как-то так это рационализируются. На самом деле, я списал из Рефала-7, когда делал вложенные функции в Простом Рефале. Но Рефал-5λ допускает и другую форму инкапсуляции — АТД-скобки. Получить доступ к содержимому можно только записав имя функции после открывающей квадратной скобки. Если имя АТД-терма не будет доступно пользователю (вроде Также потребуется определить семантику для вызова АТД-термов. Тут возможны два варианта:
Какой из них предпочесть, пока неясно. Преимуществом подхода является упрощение и языка, и компилятора. В языке получается на один конструкт меньше (объекты-замыкания), АТД-термы становятся богаче по возможностям (не только инкапсуляция). Компилятор и рантайм станут немного проще. Недостаток — существенная переделка языка и компилятора. Что выбрать?Не знаю. |
В задаче #160 рассматривается вариант, когда после специализации замыкания у него не остаётся контекста (#160 (comment)). Для такого замыкания можно или строить вырожденный объект замыкания, или заменять на указатель на функцию. В случае замены на указатель на функцию в #160 (comment) рассматривается слабое изменение поведения программы — функция Рассмотрим программу:
После специализации, прогонки и специализации замыкания мы получим следующую функцию
В неоптимизированной программе замыкание формируется один раз — в функции В оптимизированной программе присваивание встроится — конструкторы одного и того же замыкания будут находиться в аргументах В трёх ветвях функции
Из подходов, предложенных в комментарии выше, только первый (запретить дублирование замыканий) решает проблему. Если строить вырожденное замыкание вместо указателя, то проблему решает только хранение «ключей» в замыканиях. Когда я писал про подход с хранением «ключей», я неявно подразумевал, что ключами будут указатели на динамические ящики, а операция создания замыкания просто присваивает ящику новое значение (отбрасывая предыдущее). Но в случае специализации замыкания этот подход выглядит некрасивым: можно перезаписать более специализированное значение менее специализированным. Так что лучше оставить прямую генерацию ключей. Запрет на дублирование замыканий слишком жёсткий — не даёт специализировать такие функции, как |
Возможно, наилучший вариант — отказаться от инварианта, что все копии всегда равны. Для пассивных данных — равны. Если вступают в действия указатели на функции и агрессивные оптимизации — уже не определено. При этом при прогонке нужно подразумевать, что все копии всегда равны, семантика, вроде как, должна оставаться корректной. Проблема в том, что замыкания — ссылочные данные, а древесные оптимизации умеют работать только со значениями. |
Замыкания — типы значений, квадратные скобки?В последнее время я склоняюсь к мысли реализовать замыкания как квадратные скобки, как в Модульном Рефале: Преимущества и недостатки:
Можно добавить и третий вариант:
Его преимущества: по сравнению с 1 не сливается содержимое терма с аргументом, по сравнению с 2 не требуется дополнительных аллокаций памяти. |
Развитие идей предыдущего комментарияПро специализацию замыканийНаивный способ специализации замыканий работать не будетЕсли «наивно» заменить Можно придумать и более изощрённые варианты, например, угадывать специализируемые квадратные скобки по суффиксу. Имена без суффиксов или с суффиксом Сложные случаи специализации замыканийЯ пока затрудняюсь исчерпывающе описать ситуации, когда специализировать замыкания безопасно. Однако, можно выделить ситуацию, когда специализировать их опасно. Рассмотрим сначала примеры:
Из приведённых примеров можно сделать такой вывод: нельзя специализировать замыкания в экземплярах специализированных функций. Пример из #276 (comment) демонстрирует специализацию «чужого» замыкания, который попал внутрь экземпляра из сигнатуры. Второй пример демонстрирует специализацию «своего» замыкания (замыкания функции
Актуальная реализация в неоптимизированном варианте напечатает Но и в этом примере можно сказать, что выполняется специализация «чужого» замыкания — функции
Со «своими» замыканиями всё проще:
Неоптимизированная версия будет возвращать результаты в соответствии с ожиданиями: для замыканий с разным контекстом будут строиться неравные термы. Актуальная реализация всегда будет порождать неравные термы, предполагаемая для термов с равным контекстом будет возвращать равные термы. В оптимизированной версии инварианты сохранятся. Актуальная реализация для Вызов Так что следует запрещать специализацию тех замыканий, стирание одного суффикса имени которого даёт имя, отличное от имени функции, внутри которой находится замыкание. Отдельный вопрос с замыканиями в условиях — тут мне пока не очевидно. Однако обратное неверно. Можно привести пример, когда специализация «своего» замыкания опасна. (Это не комментарий сам себе противоречит, это моя мысль развивается в процессе написания.) Это слегка модифицированный первый пример:
Вызов Если в ВыводСпециализация замыканий — очень непредсказуемая оптимизация в языке, где экземпляры вложенных функций могут сравниваться на равенство. Она коварна и в актуальной реализации, и в предполагаемой, где экземпляры функций будут типами-значениями. Возможно, наилучший вариант — запретить эту оптимизацию. Возможно, единственный вариант сохранить и семантику, и эту оптимизацию — хранить в замыканиях уникальные идентификаторы (см. Но такой вариант выглядит скорее костылём, положенным между двумя стульями. Да, сидеть на нём можно, фактически занимать два стула тоже можно, но это всё равно костыль. Реализация замыканий как квадратных скобок более естественно решает проблемы семантики, за исключением специализации замыканий. Скобки каррирования?Рассмотрим случай, когда замыкания всё-таки станут типами-значениями. Семантика их вызова будет выглядеть так:
(поведение по аналогии с Тогда квадратные скобки можно назвать каррирующими скобками. Ведь они действительно строят объект, который можно вызвать и связывают префикс входного формата — несколько входных параметров. В этом дискурсе замыкание в квадратных скобках становится простым синтаксическим сахаром — компилятор неявно генерирует функцию и связывает часть её формата со значениями переменных контекста. Однако, инкапсуляция при помощи квадратных скобок становится наоборот хаком, а синтаксические ограничения на квадратные скобки повисают в воздухе. Во-первых, неочевидным становится потребность разбирать каррированное значение в образце. Во-вторых, почему, если в переменной Можно сказать, что одна конструкция языка программирования переиспользуется в двух контекстах. Это скобки инкапсуляции, но наделены свойствами, позволяющими через них реализовать замыкание. Это скобки каррирования, но их можно использовать и для сокрытия данных. Такая, своего рода, ложка-вилка, которая и как ложка не очень удобна, и как вилка. Вещь, хорошая для походных условий, но не для языка программирования. |
Сегодня в ИПМ имени М.В. Келдыша онлайн проходил семинар — я читал доклад о проблеме и предложил два варианта решения: В ходе семинара мы так и не выбрали способ решения проблемы, сошлись на том, что ни один из них не доминирует над другим. Но зато с интересом обсудили саму проблему. |
Куча проблем здесь возникает из-за того, что конструктор замыкания во время выполнения создаёт ссылочный тип данных, а во время трансформации программы рассматривается как специфический вид скобок (его можно назвать «скобки каррирования»). Соответственно, во время трансформаций его можно размножить, как и скобки данных (круглые или квадратные), каррированную функцию можно специализировать по уже известной части аргумента и т.д. Предлагается (что-то похожее писалось выше) отделить конструирование замыкания от его использования. Вместо записи
(где
«Условие» в этой записи понимается не как ограничение на предшествующий образец, а как льезонная форма записи. Переменной
В прогонке и специализации участвует не конструктор замыкания, а переменная При специализации функции в сигнатуру попадает и сама переменная
Соответственно, внутри экземпляров специализированной функции никаких специализаций замыкания быть не может. Сама функция Специализация замыкания может быть в самом выражении, содержащем замыкание. К примеру:
Здесь функция
и она успешно прогонится, а
Если при прогонке функции наружу всплывает конструктор замыкания, то он выносится в «условие-льезон»:
После оптимизации функция
Компилятор может (а вернее, должен будет) обнаруживать такие неиспользуемые замыкания и их удалять вообще. В случае размножения замыкания:
компилятор должен будет хитро генерировать код построения правой части — одно вхождение создать, другое — скопировать. Как-то так. |
Вообще, идею из предыдущего комментария нужно будет потестить на куче выкладок, сделанных ранее. Работать, вроде, должно. |
Введение. Гарантии для равенства замыканий
Сравнение на равенство является фундаментальной операцией Рефала. Синтаксис образцов допускает кратные вхождения переменных, значения которых должны быть равны. Следовательно, в ядре языка должна быть определена операция сравнения на равенство для любых типов данных.
Одна из аксиом языка (если можно так выразиться) — значения, полученные путём копирования (кратные вхождения в результатное выражение) должны быть равны в смысле сопоставления с кратными переменными образца.
Например:
Функция
F
всегда должна возвращатьTrue
.Ещё есть неявная аксиома — повторные s-переменные должны сопоставляться за константное время. В учебнике Турчина говорится, что открытые переменные, повторные t- и e-переменные скрывают за собой рекурсию, следовательно, виды сопоставлений рекурсию не скрывают, т.е. сопоставляются за константное время.
Рефал-5λ поддерживает вложенные функции в результатных выражениях. Во время выполнения для вложенных функций порождаются замыкания — значения типа «символ» (сопоставимые с s-переменными), которые можно вызывать как функции. Замыкания могут захватывать контекст.
Возникает вопрос: как сравниваются на равенство два замыкания? Для Простого Рефала в
manul.pdf
были определены следующие ограничения:Примечание. Следует уточнить понятие текстуально разные. Текстуально разными считаются не только блоки в разных позициях в одном файле, но и один и тот же блок в заголовочном файле, включённый в разные единицы трансляции.
То, что не описано выше, намеренно не определено. В частности, если замыкание с одним и тем же контекстом из одного и того же текстуально блока создаётся в разные моменты времени, то равенство не определено. Простейший пример:
Функция
F1
создаёт замыкание с пустым контекстом, а для таких случаев вместо объекта замыкания создаётся просто указатель на неявную глобальную функцию (&F1\1
). Поэтому первый вызов распечатаетTrue
. Так было сделано с самой первой реализации вложенных функций вSimple Refal.004
.Второй вызов по умолчанию распечатает
False
, поскольку два вызова<F2 e.X>
создадут два объекта замыкания, которые сравнятся по ссылке. Но если функцииF2
иEq
прогнать (или даже встроить), то мы получим<Prout True>
. Реализованный алгоритм обобщённого сопоставления с образцом допускает повторные переменные любого типа, причём сопоставление считается успешным, если значения этих переменных текстуально совпадают (если не совпадают и тип переменной не s — результат не определён). Поэтому здесь сопоставление будет успешным.Далее, мы рассмотрим три примера. Один с мнимым нарушением семантики, два других — с реальным.
Воспроизведение ошибки
Пример 1. Мнимое нарушение семантики при прогонке
Скачать: closures-neq-drive.ref.
Это случай неопределённого поведения, когда сравниваются два замыкания из одного и того же блока, с равными контекстами, но созданные в разное время.
Пример 2. Реальное нарушение семантики при прогонке
Скачать: closures-eq-drive.ref.
Здесь встраивается вызов
Dup
, в результате чегоEq
вызывается с аргументомВо время выполнения создаются два одинаковых объекта замыкания. Но, поскольку замыкания сравниваются по ссылке, они оказываются не равны.
Пример 3. Реальное нарушение семантики при специализации
Скачать: closures-neq-spec.ref.txt.
Причина похожа на предыдущую — строятся два одинаковых экземпляра замыкания вместо копирования одного. Только теперь путём размножения значения статической переменной. Специализированная функция
S@1
выглядит так:Решение
А вот однозначного решения тут пока не видно — везде есть компромиссы.
Приемлемыми мне видятся только последние три варианта. А может даже, последние два.
Буду думать летом после завершения #260, #256, #252, #253.
The text was updated successfully, but these errors were encountered: