Open Mazdaywik opened 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
. На Си++ можно использовать шаблонные обёртки.
Оба варианта избавляют от необходимости писать обёртку для каждой функции распределения с оверхедом на отдельный вызов.
Нюанс или «а слона-то я и не приметил».
Выше в комментарии я написал, что библиотека Library.ref
не создаёт локальных переменных с нетривиальными деструкторами.
Но создаёт их сам рантайм! Причём — в самой функции main_loop()
, где и предполагается использовать setjmp()
:
Я попробовал перехватывать ошибки памяти при помощи setjmp()
, думал, что пронесёт. Но тест limit-memory.FAILURE.sref
стал глючить.
(Коммит пока в локальной ветке, не на сайте GitHub.)
Так что нужно избавляться от локальных объектов. Тем более, что отладчик внутри цикла совершенно неуместен — для условий в нативном коде рекурсивно вызывается main_loop()
и экземпляр отладчика будет создаваться каждый раз новый. Что неправильно.
Для последнего коммита был сделан замер стандартным бенчмарком. Уменьшение цикла самоприменения (медиана) составило ≈0,05 секунд:
Так что прирост производительности недостоверный. Во втором замере были заметные выбросы, перемеривать лень.
Для последнего коммита сделан анализ производительности — до него (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 % от общего выполнения программы. Так что его оптимизация тоже мало что даёт.
Предыдущий коммит ускорил время работы программы на 10 % (Total refal time), хотя является чистым рефакторингом!
Компьютер, ОС и компилятор Си++ те же, что и выше. Переменные SREFC_FLAGS
, SRMAKE_FLAGS
и SCRIPT_FLAGS
пустые.
Вывод: BCC 5.5.1 не догадывался заинлайнить даже такие тривиальные функции. Урок для моего компилятора — встраиваемые функции также нужно уметь выделять синтаксически.
Прикреплю замеры времени на память.
Исходя из логики, следующий коммит, оптимизирующий link_adjacent
, тоже должен дать измеримый прирост.
Действительно, есть измеримый прирост по сравнению с предыдущим коммитом. Но численно чуть меньше — всего 7,8 %. Может быть, из-за того, что я компилировал BCC 5.5.1 без оптимизаций и он не встроил этот вызов. Но не суть, главное — ускорение.
Для истории — замер.
Пояснение. Почему цифры меньше, чем в подробном замере выше? Потому что тогда использовался QueryPerformanceCounter()
ради большей точности результатов, а он медленный. Здесь результаты заметны без точных часов, поэтому QPC не использовался.
Предыдущий коммит содержит замеры производительности. Они выполнялись на машине Intel® Core™ i5-2430M CPU @ 2.40 GHz, оперативная память 8 Гбайт, стандартным бенчмарком, 9 замеров.
Вот замеры (пустые строки были добавлены для удобства чтения):
Эта задача — подзадача для #204, а та назначена на ВКР.
Задача не была выбрана в качестве темы ВКР.
Эта задача — подзадача #185 (UPD: и #204 тоже). Процитирую параграф оттуда.
В Рефале-05 при ошибке отождествления и недостатке памяти программа аварийно останавливается. Рефал-5λ не может позволить себе такую роскошь — в случае аварийной ситуации программа может продолжать выполняться, например, если программа имеет несколько доменов или несколько виртуальных машин. Например, может быть реализована функция-песочница, которая запускает другую функцию и возвращает либо результат работы, либо признак ошибки.
Поэтому останавливать программу нельзя, нужно как минимум делать нелокальный переход (исключение или
longjmp()
). Распределение памяти может выполняться и из пользовательских нативных функций. А там могут распределяться ресурсы, требующие дальнейшего освобождения (например, память или открытые файлы).Если использовать исключения и RAII-ресурсы, то на первый взгляд проблем нет. Кроме случая использования разных DLL с разными рантаймами Си++.
Поэтому по-прежнему остаются нужны API-функции создания элементов, которые могут возвращать признак успешности. А также нужно сохранить
return …NO_MEMORY;
. Кстати, признак успешности не обязательно должен быть кодом возврата. Функция может возвращатьvoid
, но при этом в API может быть и функция с именем вродеbool alloc_failed()
, которая возвращаетtrue
, если один из предыдущих вызовов память выделить не смог.Таким образом, можно выделить две подзадачи: