bmstu-iu9 / refal-5-lambda

Компилятор Рефала-5λ
https://bmstu-iu9.github.io/refal-5-lambda
Other
78 stars 35 forks source link

Построение результатных выражений как в Рефале-05 #196

Open Mazdaywik opened 5 years ago

Mazdaywik commented 5 years ago

Эта задача — подзадача #185 (UPD: и #204 тоже). Процитирую параграф оттуда.

Компиляция результатных выражений в Рефале-05 отличается от Рефала-5λ двумя новшествами:

  • Используется допущение о последовательном распределении памяти в списке свободных узлов.
  • Функции выделения памяти всегда успешны.

Допущение о последовательном распределении узлов для меня (@Mazdaywik) не ново — я так делал и в Модульном Рефале.

Суть его состоит в том, что следующие друг за другом операции распределения памяти создают в списке свободных узлов значения, располагающиеся последовательно в том же порядке. После чего в специально заготовленные места (в список свободных узлов) переносятся значения переменных и в результате там получается образ результатного выражения. Образ переносится в поле зрения командой splice_from_freelist, которая размещает его перед открывающей угловой скобкой. Затем остаток от вызова функции переносится в список свободных узлов командой splice_to_freelist.

У этого подхода два преимущества: повышается быстродействие и упрощается генерация кода. И то, и другое обеспечивается отсутствием команд переноса построенных узлов по отдельности.

В текущей реализации Рефала-5λ функции распределения памяти возвращают булевское значение: истину, если память распределить удалось, и ложь, если памяти оказалось недостаточно. В корректных программах истина возвращается всегда, а значит, всегда выполняется избыточная проверка. Если на стадии распределения памяти памяти оказалось недостаточно, то функция возвращает признак ошибки cNoMemory, в ответ на который рантайм выводит дамп и аварийно останавливает программу.

В Рефале-05 операции распределения памяти выполняются всегда успешно — в API не предусмотрено никакой проверки на недостаток памяти. Если операция аллокации память выделить не смогла, то она сама прерывает программу с выводом аварийного дампа.

Этот подход разумно перенести на Рефал-5λ. Останавливать ли программу в самой функции распределения или использовать нелокальный переход (исключение или longjmp()) — следует решить.

В Рефале-05 при ошибке отождествления и недостатке памяти программа аварийно останавливается. Рефал-5λ не может позволить себе такую роскошь — в случае аварийной ситуации программа может продолжать выполняться, например, если программа имеет несколько доменов или несколько виртуальных машин. Например, может быть реализована функция-песочница, которая запускает другую функцию и возвращает либо результат работы, либо признак ошибки.

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

Если использовать исключения и RAII-ресурсы, то на первый взгляд проблем нет. Кроме случая использования разных DLL с разными рантаймами Си++.

Поэтому по-прежнему остаются нужны API-функции создания элементов, которые могут возвращать признак успешности. А также нужно сохранить return …NO_MEMORY;. Кстати, признак успешности не обязательно должен быть кодом возврата. Функция может возвращать void, но при этом в API может быть и функция с именем вроде bool alloc_failed(), которая возвращает true, если один из предыдущих вызовов память выделить не смог.

Таким образом, можно выделить две подзадачи:

Mazdaywik commented 5 years ago

API рантайма Рефала-5λ используется только в библиотеках Library.ref и Hash.ref (и некоторых автотестах) и в библиотеке Модульного Рефала. Я проверил все вызовы функций refalrts::alloc_…, можно ли их заменить «безотказными» функциями, которые делают longjmp() при недостатке памяти.

В Library.ref так сделать можно — ни в одной из функций, вызывающих refalrts::alloc_…, не используются объекты Си++ с деструкторами. Замена будет совершенно безопасной и упростит код библиотеки.

В библиотеке Модульного Рефала всё немножко интереснее. В некоторых функциях, где выполняется распределение памяти, существуют переменные классов refalapi::CharArray и refalapi::AllocArray. Первый класс является обёрткой над std::vector<char>, второй не нужен.

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

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

Значит, функции распределения, возвращающие bool, всё-таки нужны. Но, чтобы не дублировать каждую из функций, можно пойти двумя путями.

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

Можно пойти ещё гибче, написать функцию-обёртку. Для Си она будет иметь тип

FnResult checked_alloc(FnResult (*func)(void *data), void *data);

Соответственно, она вызывает функцию func, передавая ей указатель на что-то. Если вызванная функция завершилась успешно — возвращается её код возврата. Иначе возвращает cNoMemory. На Си++ можно использовать шаблонные обёртки.

Оба варианта избавляют от необходимости писать обёртку для каждой функции распределения с оверхедом на отдельный вызов.

Mazdaywik commented 5 years ago

Нюанс или «а слона-то я и не приметил».

Выше в комментарии я написал, что библиотека Library.ref не создаёт локальных переменных с нетривиальными деструкторами.

Но создаёт их сам рантайм! Причём — в самой функции main_loop(), где и предполагается использовать setjmp():

https://github.com/bmstu-iu9/refal-5-lambda/blob/210ab9731372a3bfcbec2a6e35e1249aba71badb/src/srlib/refalrts-vm.cpp#L603-L610

Я попробовал перехватывать ошибки памяти при помощи setjmp(), думал, что пронесёт. Но тест limit-memory.FAILURE.sref стал глючить.

https://github.com/bmstu-iu9/refal-5-lambda/blob/210ab9731372a3bfcbec2a6e35e1249aba71badb/autotests/limit-memory.FAILURE.sref#L1-L5

(Коммит пока в локальной ветке, не на сайте GitHub.)

Так что нужно избавляться от локальных объектов. Тем более, что отладчик внутри цикла совершенно неуместен — для условий в нативном коде рекурсивно вызывается main_loop() и экземпляр отладчика будет создаваться каждый раз новый. Что неправильно.

Mazdaywik commented 5 years ago

Для последнего коммита был сделан замер стандартным бенчмарком. Уменьшение цикла самоприменения (медиана) составило ≈0,05 секунд:

Так что прирост производительности недостоверный. Во втором замере были заметные выбросы, перемеривать лень.

Mazdaywik commented 5 years ago

Для последнего коммита сделан анализ производительности — до него (90101b3003df10045c99e689b7fecf7b9683bf67) и на нём (e39b8b6635d4ae5cded350fbdb45a3b66a8bc339). Прирост оказался меньше, чем я ожидал: 1–2 %.

Выполнялся 21 замер стандартным бенчмарком, компилятор BCC 5.5.1 (без ключей оптимизации), процессор Intel® Core™ i5-5200U CPU @ 2.20 GHz, кэши L1/L2/L3 128 Кбайт/512 Кбайт/3 Мбайта. ОС Windows 10 x64, памяти достаточно (8 Гбайт), жёсткий диск жёсткий.

Замер не совсем точный, поскольку изменилась кодогенерация. В частности, из-за этого число шагов уменьшилось на 0,05 %.

Замер режима интерпретации

Ключи компилятора

set SREFC_FLAGS=-F-DREFAL_5_LAMBDA_USE_QPC
set SRMAKE_FLAGS=-X-F-DREFAL_5_LAMBDA_USE_QPC
set SCRIPT_FLAGS=

Результаты замера:

Интерпретация:

Остальные метрики ничего интересного не дают, да и давать не должны.

Замер режима прямой кодогенерации

Ключи компилятора

set SREFC_FLAGS=-F-DREFAL_5_LAMBDA_USE_QPC -Od
set SRMAKE_FLAGS=-X-F-DREFAL_5_LAMBDA_USE_QPC -X-Od --runtime=refalrts-diagnostic-initializer
set SCRIPT_FLAGS=--scratch

Результаты замера:

Интерпретация:

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

Резерв оптимизации

На сколько можно ускорить код, оптимизируя построение результатных выражений в духе Рефала-05? Очевидно, быстрее оптимизации результатных выражений (-OR) не получится.

Сделал дополнительные замеры с ключами

Первый

set SREFC_FLAGS=-F-DREFAL_5_LAMBDA_USE_QPC
set SRMAKE_FLAGS=-X-F-DREFAL_5_LAMBDA_USE_QPC

Второй

set SREFC_FLAGS=-F-DREFAL_5_LAMBDA_USE_QPC -OR
set SRMAKE_FLAGS=-X-F-DREFAL_5_LAMBDA_USE_QPC -X-OR

Для чистого времени Рефала ускорение составляет 23 %, от 16,20416 секунд (84 %) до 12,44449 секунд (81 %). Доверительные интервалы понятно что не пересекаются.

Для линейного времени построения результатных выражений — 49 %, от 8,22838 секунд (43 %) до 4,19436 (27 %).

Полное время выполнения программы ускоряется на 20 %, от 19,21300 секунд до 15,36468 секунд.

Вывод

Получен измеримый прирост быстродействия. Но очень маленький, что немного обидно. Манипуляции с построением результатных выражений не смогут ускорить программу более чем на 20 %, чистое время Рефала — на 25 %, линейное построение результатных выражений — на 50 %. Время копирования контекста и te-переменных в стандартном бенчмарке составляет в сумме ≈5 % от общего выполнения программы. Так что его оптимизация тоже мало что даёт.

Mazdaywik commented 5 years ago

Предыдущий коммит ускорил время работы программы на 10 % (Total refal time), хотя является чистым рефакторингом!

Компьютер, ОС и компилятор Си++ те же, что и выше. Переменные SREFC_FLAGS, SRMAKE_FLAGS и SCRIPT_FLAGS пустые.

Вывод: BCC 5.5.1 не догадывался заинлайнить даже такие тривиальные функции. Урок для моего компилятора — встраиваемые функции также нужно уметь выделять синтаксически.

Прикреплю замеры времени на память.

Исходя из логики, следующий коммит, оптимизирующий link_adjacent, тоже должен дать измеримый прирост.

Mazdaywik commented 5 years ago

Действительно, есть измеримый прирост по сравнению с предыдущим коммитом. Но численно чуть меньше — всего 7,8 %. Может быть, из-за того, что я компилировал BCC 5.5.1 без оптимизаций и он не встроил этот вызов. Но не суть, главное — ускорение.

Для истории — замер.

Mazdaywik commented 5 years ago

Пояснение. Почему цифры меньше, чем в подробном замере выше? Потому что тогда использовался QueryPerformanceCounter() ради большей точности результатов, а он медленный. Здесь результаты заметны без точных часов, поэтому QPC не использовался.

Mazdaywik commented 4 years ago

Предыдущий коммит содержит замеры производительности. Они выполнялись на машине Intel® Core™ i5-2430M CPU @ 2.40 GHz, оперативная память 8 Гбайт, стандартным бенчмарком, 9 замеров.

Вот замеры (пустые строки были добавлены для удобства чтения):

Mazdaywik commented 3 years ago

Эта задача — подзадача для #204, а та назначена на ВКР.

Mazdaywik commented 3 years ago

Задача не была выбрана в качестве темы ВКР.