-
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
Специализация замыканий #160
Comments
Один из возможных подходов к решению этой задачи — перенести оптимизацию внутрь прохода обессахаривателя до «расплющивания» вложенных функций. Тогда не будет проблем с конструкцией каррирования, а подстановка будет осуществляться естественным образом. Но проблема этого подхода в том, что выразить специализацию будет гораздо сложнее. Конструктор каррирования — это просто особый тип скобок, который при специализации «уходит» в сигнатуру, а специализированная функция будет просто принимать переменные-аргументы конструктора каррирования. Если вместо конструктора каррирования хранить тело вложенной функции, то при специализации придётся вычислять его контекст явным образом, т.е. принудительно его «сплющивать» раньше времени. Поэтому в качестве рабочего варианта предлагается специализировать конструкторы каррирования тем же движком, который специализирует функции. |
А ведь специализация функций в некотором смысле эквивалентна прогонке, и даже встраиванию. Пусть специализируемая функция имеет вид:
Тогда её специализацию можно свести к встраиванию функции
При этом вызовы Но это вовсе не значит, что задача потеряла актуальность. Ведь
Вызовы Т.е. от специализации всё равно не убежать. |
Я просто оставлю это здесь (чтобы не потерялось):
Его можно откомпилировать, запустить и оно четыре раза правильно посчитает факториал от 10. Но как оно работает, я понимаю туманно. |
Над задачей буду работать только я, поскольку Дарья ушла в академический отпуск. Соответственно, веху study spring 2018 снимаю. |
Где-то ранее предлагалось все вложенные функции по умолчанию считать прогоняемыми. Предлагается также все вложенные функции считать и специализируемыми тоже — переменные контекста — это и есть специализируемые параметры. Такой взгляд будет особенно продуктивен в условиях и блоках, где эти функции непосредственно вызываются. Специализация делает функцию на Рефале многоместной — местность, во-первых, определяется числом параметров в объявлении При генерации кода требуется многоместную функцию вновь сделать одноместной. Способ известен — если есть N штук e-параметров, то достаточно (N−1) из них завернуть в скобки. Предлагается заворачивать в скобки все e-параметры, кроме последнего — это упростит специализацию конструкторов замыканий. Конструктор замыкания вида
для оптимизатора представляется в виде фиктивного вызова
где
Параметр
то получим специализированный конструктор замыкания. |
Исключил из вехи, в задачу диплома не входит. |
Это не учебная задача. Снял метку «study». |
Конкретные шаги:
Забавно, что задачу решает только пункт 5, всё остальное — доработка окружения (как часто и бывает с правками больших систем). Пункты 1 и 2 обязательны — сделать сразу последний пункт без этих правок невозможно. Тупо или не будет работать (будут получаться имена функций с двумя Пункты 3 и 4 технически избыточны, и без их выполнения всё будет работать. Разрешать одновременную прогонку и специализацию (3) имеет смысл, поскольку теперь уже это будет поддерживаться на back-end’е (замыкания ведь они неявно Но эту задачу следует отложить. На данный момент ведётся работа #253, которая существенно затрагивает специализатор. А выполнение данной задачи специализатор несколько усложнит. |
Вопрос: что делать, если при специализации замыкания полностью исчезает контекст? Тут есть существует два варианта:
Преимуществом генерации указателя является производительность, недостатком — тонкое изменение поведения программы: указатель на функцию меняет тип. У вырожденного объекта замыкания преимущества и недостатки зеркальны генерации указателя. В идеале все оптимизации должны быть прозрачны: оптимизированная и неоптимизированная программы различаются только быстродействием. Но оптимизация прогонки может удалять шаги рефал-машины, а значит, вызов
Вывод программы будет меняться в зависимости от ключа Кроме того, замена специализированного замыкания без контекста усложнит исправление #276 (см. #276 (comment)), |
Замена «квадратной» квадратичной сложности на «прямоугольную». Оптимизация построения результатных выражений использует алгоритм нечёткого жадного строкового замощения (GST), который имеет квадратичную сложность. Пусть P — образец предложения, R — результат, |•| — длина в токенах. Ранее сложность алгоритма GST составляла O(max(|P|, |R|)²), теперь O(|P|×|R|), что гораздо эффективнее для случаев, когда длины левой и правой части сильно различаются. Например, в функции PrintHelp из ParseCmdLine левая часть имеет вид <PrintHelp>, правая — <Prout 'длинная строка'>, из-за чего компилятор при обработке этого файла задумывался на минуту. Был выполнен замер производительности. Стандартный бенчмарк, 9 итераций, для экономии времени были указаны режимы RLC_FLAGS=-ODPRS, RLMAKE_FLAGS=-X-ODPRS, BENCH_FLAGS=-OR. • Полное время работы программы: 98,797 [98,734…98,906] → 34,141 [34,109…34,203], ускорение более чем достоверное, 65 % или 2,9 раза! • Число шагов: 89 386 191 → 37 819 100, 58 % или 2,4 раза! Ранее в рейтинге профилировщика первые строчки занимали следующие функции: ZipItems (9074) -> 26827.0 ms (23.90 %, += 23.90 %) DoOverlapOffsets (9074) -> 15801.0 ms (14.08 %, += 37.98 %) OverlapItem (9074) -> 14437.0 ms (12.86 %, += 50.84 %) GlueTiles (9074) -> 14169.0 ms (12.62 %, += 63.46 %) которые суммарно требовали 2/3 времени работы программы. Теперь они убежали вниз. Теперь рейтинг такой: Apply (9074) -> 3208.0 ms (6.94 %, += 6.94 %) FilterResultPos (9074) -> 3080.0 ms (6.66 %, += 13.60 %) FilterPatternPos (9074) -> 2710.0 ms (5.86 %, += 19.47 %) Map (9074) -> 1992.0 ms (4.31 %, += 23.78 %) ZipItems (9074) -> 1719.0 ms (3.72 %, += 27.50 %) DoOverlapOffsets (9074) -> 1602.0 ms (3.47 %, += 30.96 %) Map@3 (9074) -> 1590.0 ms (3.44 %, += 34.40 %) OverlapItem (9074) -> 1289.0 ms (2.79 %, += 37.19 %) Вызов Apply не оптимизировался, поскольку его аргумент был активным, а активные вызовы сейчас не прогоняются (#230). Функции FilterResultPos и FilterPatternPos могли бы встроиться (вернее, только второй), но они не помечены как прогоняемые, а вручную я помечать их пока не хочу. Они не рекурсивные и поэтому будут прогоняться после выполнения #252. Функция Map не специализировалась по FilterResultPos по той же причине, что и Apply — она осталась внутри замыкания, создаваемого Pipe. Тут, возможно, могла бы помочь специализация замыканий #160. В общем, эти функции должны сами оптимизироваться. Подытоживая: теперь режим -OR стал гораздо привлекательнее.
Имена функций с меткой (Spec …) в дереве могут иметь суффиксы. На данный момент это совершенно нейтральное изменение, никакими входными данными его выявить невозможно, поэтому с полным основанием оно является рефакторингом. Но это исправление нужно сразу нескольким задачам: • Вложенные функции (включая присваивания и блоки) неявно преобразуются в обычные глобальные функции с суффиксами. Задача #160 требует их специализировать. • В задаче #252 предлагается автоматически размечать функции, пригодные для оптимизации (включая специализацию). Размечатель будет работать как с входным деревом до оптимизации, так и с оптимизированным деревом после оптимизаций. Пригодными для оптимизации могут быть и функции с суффиксами. • При глобальной оптимизации (#255) синтаксические деревья разных единиц трансляции объединяются в одно, локальные функции в них переименовываются — получают суффиксы. • В задаче #251 предлагается рассмотреть специализацию всех функций программы, включая функции с суффиксами. • На это нет отдельной заявки (это часть #160), но имеет смысл разрешить одновременную прогонку и специализацию функций, пометку одной функции метками $DRIVE и $SPEC одновременно. В этом случае придётся специализировать и функции Func*n — остатки прогоняемых функций. Ну и кроме того, это красиво: поддержка не частного случая, а общего.
Нужно различать два случая:
Первую оптимизацию можно добавить хоть сейчас, она не помешает ничему. Функции всё равно будут оптимизироваться в позиции вызова, поэтому стирание призрачной скобки ни на что не повлияет. Вторая оптимизация требует переписывания разметки призрачных скобок. Убрать ошибку одновременной прогонки и специализации тоже легко. Но без специализации |
Остальные призрачные скобки становятся обычными.
Теперь, благодаря специализации замыканий, можно указанные функции-циклы переписать в более естественном стиле. Функция Reduce получила дополнительный контроль. На быстродействии самого компилятора это сказаться не должно, поскольку Reduce используется гораздо реже, чем другие функции-циклы. Для некоторых вызовов MapAccum и Reduce было проверено в логе, что эти функции правильно специализируются.
Если Fetch не может встроиться из-за того, что вызов активный (#230), то он хотя бы специализируется.
Вместо того, чтобы тянуть вола за хвост, решил добить эту задачу. Правки в Альтернативный вариант: распознавать не только вызовы с совпадающими именами, но и вызовы функций с суффиксами Замеры производительностиБыл выполнен стандартный бенчмарк на коммитах 4833a73 (до правок), 4fa2160 (рефакторинг Компьютер: процессор Intel® Core™ i5-2430M? 2,40 ГГц, ОЗУ 8 Гбайт, диск SSD. ОС Windows 10 x64. Компилятор C++ — BCC 5.5.1. Выполнялось 13 замеров со следующими опциями: set RLMAKE_FLAGS=-X-ODS
set BENCH_FLAGS=-ODPR Опции были выбраны из следующих соображений: На первой паре замеров получился измеримый прирост производительности, преимущественно за счёт Замеры до:
Вторые замеры:
Прирост производительности:
Преимущество достигнуто за счёт ключа
После оптимизации:
Видно, что сначала были не прооптимизированы вызовы refal-5-lambda/src/compiler/GST.ref Lines 260 to 278 in d809981
До оптимизаций она трансформировалась так (аварийные предложения убрал для наглядности):
Видно, что не смотря на то, что вызов И тут я подумал: а может попробовать специализировать и
Метрики:
Результат предсказуемый, ведь
ВыводыПрирост производительности небольшой и только за счёт оптимизации специфического кода — стопок вызовов Вообще, эта заявка скорее эстетическая, нежели практическая. Не уверен, что найдётся много кода, где эти правки дадут заметный прирост производительности. Но компилятор стал в некотором смысле цельнее:
Закрываю задачу. |
Если дословно выполнить задания #122 и #126, оптимизатор не будет давать того результата, который хотелось бы (а будет немного хуже). Поясню на примере
Пример
Хотелось бы получить вот такой результат:
Во всех примерах выше была сделана подстановка переменных внутрь замыкания — были построены новые замыкания. Но в актуальной постановке задачи этого не будет. Потому что обессахариватель.
Обессахариватель преобразует программу на Рефале-5λ (или Простом Рефале) в программу на базисном Рефале с условиями (#17) и конструктором каррирования. Сейчас условия нас не интересуют, а вот конструктор каррирования играет первостепенную роль.
Каррированием в математике (лямбда-исчислении, комбинаторной логике и смежных дисциплинах) называется представление функции из N аргументов x1, x2, …, xN как функции одного аргумента x1, которая возвращает функцию одного аргумента x2, которая возвращает функцию одного аргумента x3, … , которая возвращает функцию аргумента xN, которая уже возвращает возвращаемое значение исходной функции.
Конструктором каррирования в промежуточном представлении называется узел
(#ClosureBrackets e.ClosureContext)
, который в скомпилированном коде превращается в операцию создания замыкания.e.ClosureContext
обязан начинаться с имени глобальной функции, остальная часть контекста должна быть пассивной — может состоять только из переменных, структурных и абстрактных скобок и атомов (хотя вроде ничего не должно сломаться, если там окажется вызов функции, но я не гарантирую). В псевдокоде конструктор каррирования будем обозначать как{{ &Func контекст }}
, где&Func
— имя некоторой глобальной функции.На выходе из обессахаривателя листинг 1 превратится в
Вложенные функции превратились в глобальные, при этом в левые части добавились новые параметры, соответствующие захваченным переменным. На местах вложенных функций в левых частях обессахариватель поместил конструкторы каррирования, создающие замыкания из глобальных функций путём связывания части аргумента с актуальными значениями захватываемых переменных.
После оптимизации программа приобретёт следующий вид:
Результат гораздо скромнее. Подстановки переменных повлияли только на конструктор каррирования, сами вложенные функции они не затронули никак.
Что делать
Для этого случая вполне можно создать решение ad hoc — обнаруживая подстановку в конструктор каррирования, сразу же пытаться построить специализированный вариант каррируемой функции. Задача облегчается тем, что имя переменной в конструкторе совпадает с именем переменной в каррируемой функции.
Достоинство: не зависит от задачи #126, сохраняет разметку контекста для отладки.
Недостаток: частично дублирующаяся логика.
Другой вариант — дождаться решения задачи #126 и для специализации каррирований использовать уже готовый механизм специализации функций.
Достоинство: логика не дублируется, повторно используется более общий механизм.
Недостаток: реализация специализатора может устранять константные элементы в формате, в итоге сотрётся разметка контекста.
Нюанс
Каркас оптимизатора, построенный в задаче #155, уже содержит логику оптимизации — удаляет конструкторы каррирования после угловой скобки:
В итоге каррированный объект разрушается и становится нечего специализировать.
Изначально это было сделано для упрощения оптимизации встраивания и прогонки. Вложенные функции по умолчанию являются прогоняемыми. Если снимать с них скобки каррирования внутри скобок вызова, то прогонщику достаточно будет рассматривать только вызовы вида
<F …>
. Если не снимать, то прогонщик должен рассматривать и вызовы<F …>
, и вызовы<{{&F …}} …>
.К тому же скобки каррирования впринципе избыточны внутри скобок вызова, и их в принципе снимать там полезно.
Чтобы облегчить задачу специализации, можно отказаться от снятия скобок между проходами оптимизаторов. Хуже не будет, если снимать их в самый последний момент. С другой стороны, сохранение конструктора каррирования может даже упростить прогонщик. В вызовах
<{{&F …}} …>
функцияF
будет считаться всегда прогоняемой, а значит не потребуется отдельная пометка вложенных функций прогоняемых.Важность
Пример кода выше выглядит и является надуманным, и поэтому может показаться, что специализация замыканий — интеллектуальная игра и «экономия на спичках». Это не так.
Присваивания и блоки Рефала-5λ являются синтаксическим сахаром, их рассахариватель преобразует во вложенные функции. Поэтому если оптимизированная функция содержит присваивание, то дальше присваивания оптимизация просто не пройдёт.
В
WithDriveAndAssign
вызов функцииRegex
полностью прогонится, но подстановки переменнойe.Text
за присваивание не протекут. В случаеMapReduce
всё ещё хуже: за первое присваивание не протечёт подстановкаs.Fn
— вызов во втором присваивании будет в общем положении и не будет специализирован.План
$INLINE
/$DRIVE
и$SPEC
больше не должно быть ошибкой.Func*M
, если имяFunc
— специализируемое. ФункцияFunc*M@N
должна образовываться путём вычёркивания первыхM
предложений.(Spec …)
в рассахаривателе.TrySpecCall
для замыканий.The text was updated successfully, but these errors were encountered: