Telegram Web
Недавно завершилась конференция C++ Russia 2023 и пришло время подвести некоторые ее итоги. Разумеется, в контексте нашего канала, благо на конференции появилась отдельная секция по компиляторным докладам.

Теперь о двух докладах, на которых я присутствовал в том или ином качестве.

Loop unrolling в деталях Ивана Афансьева. Доклад, как это понятно из названия, посвящен теме раскрутки циклов в компиляторе. Это введение в предмет, более интересное, пожалуй, начинающим. Правда, есть один любопытный нюанс. В завершающей части доклада Иван переходит от игрушечных задач к задаче из своей практической деятельности. Это, совершенно неожиданно, оказывается NP-полная задача о вершинном покрытии графа. Боюсь, что далеко не все начинающие поняли тонкости задачи в процессе доклада. Откуда взялась эта задача? Этот вопрос я и задал Ивану. Тут-то и выяснилось самое интересное. Оказывается, это задача относится к проекту генератора кода для спецпроцессора. Иван решает ее в лоб для малого набора данных (раскручивает циклы, использует векторизацию), а если набор данных велик -- использует ансамбль из эвристических алгоритмов. Достаточно впечатляюще! Здесь я пожалел, что Иван не сделал на эту тему отдельного доклада уже для опытных компиляторщиков.

Автоматизация программирования в СССР. Трансляторы (60–70-е годы). Вторая часть моего доклада. В процессе подготовки от ряда тем пришлось отказаться, поскольку на их изложение времени уже не хватало. В этой части я постарался еще более явно связать древние времена с самыми современными компиляторными подходами и в этом смысле, боюсь, доклад не совсем будет понятен начинающим. Впрочем, возможно я и ошибаюсь, так что предлагаю самостоятельно посмотреть и оценить. За возможность дать ссылку на yt благодарю Тимура Сафина и JUG Ru Group.

https://www.youtube.com/watch?v=Q2ErYDuVAWo
Прерываю затянувшуюся тишину на нашем канале! Надеюсь, остальные участники PLComp тоже, со временем, активизируются.

Недавно я выступил с двумя компиляторными докладами. Рассказал о том, что мне самому было бы интересно услышать.

Доклад Graph-Based Intermediate Representations: An Overview and Perspectives. Введение и обзор графовых IR. Такие IR диктуют оригинальный взгляд на построение компиляторов в целом. Впрочем, до сих пор эти IR являются, скорее, экзотикой, поэтому доклад будет более интересен специалистам.

Видеозаписи у меня нет, но имеются слайды: https://github.com/true-grue/graph-irs

Доклад В Python есть готовый фронтенд для вашего компилятора. Компиляторные технологии для начинающих в Питоне. О том, как сочетание модуля ast и сопоставления с образцом дает возможность по-новому взглянуть на задачу построения DSL. Кстати, доклад должен быть интересен не только питонистам, ведь примеры (каждый из которых содержит <100 строк кода) включают в себя DSL-компилятор Datalog и компилятор в Wasm.

Видеозапись: https://www.youtube.com/watch?v=h-TzDPL2nDE

Материалы к докладу: https://github.com/true-grue/python-dsls
Видеозапись моего недавнего выступления. Хотя тема и узкоспециальная, все же, предлагаю досмотреть. Мне кажется, во второй-третьей части доклад становится интереснее и для тех компиляторщиков, кому заявленная тематика не близка. Отдельно благодарю представителей компании YADRO за отличные вопросы!

https://www.youtube.com/watch?v=Q6-h6R_e-04
Коллеги, друзья и единомышленники!

Трудно поверить, но нашему каналу PLComp (Programming Languages & Compilers) уже исполнилось 4 года. За это время мы подготовили почти сотню обзорных заметок, освещающих историю развития компиляторов, структуру современных и перспективных компиляторов, а также смежные темы. Мы искренне надеемся, что наши читатели смогли найти в этих материалах что-то интересное и полезное для себя.

К сожалению, в последние год-два по различным причинам количество новых заметок сократилось, и мы начали задумываться о возможном закрытии проекта. В связи с этим нам бы хотелось задать нашим читателям несколько открытых вопросов:

1. Какие из опубликованных материалов вам запомнились больше всего?

2. Считаете ли вы, что нам стоит продолжить работу над каналом?

3. Какой формат подачи информации кажется вам наиболее подходящим и удобным?

4. Хотели бы вы принять участие в будущих публикациях на PLComp? (лучше ответить личным сообщением)

Мы будем признательны за любые комментарии, идеи и предложения.
📚 Breno Campos, Ferreira Guimarães
"Lazy Evaluation for the Lazy: Automatically Transforming Call-by-Value into Call-by-Need"
Proceedings of the 32nd ACM SIGPLAN International Conference on Compiler Construction. February 2023: 239–249


Ленивыми называются такие вычисления, которые программа не производит сразу, а откладывает, пока не потребуется их результат. Если выполнять редкие вычисления лениво, средняя производительность программы может вырасти.

Чтобы понять, какие вычисления выгодно производить лениво, программист делает предположения о сценариях выполнения программы или профилирует её. В языках с поддержкой ленивых вычислений можно просто пометить фрагмент программы как lazy, но во многих, особенно низкоуровневых языках, добиться ленивости сложнее, и это может потребовать существенной модификации кода.

Цель авторов — научить компиляторы находить вычисления, которые предпочтительно сделать ленивыми, не используя аннотирование lazy вручную и даже поддержку ленивости со стороны языка. Для этого они реализовали внутри LLVM специальную profile-guided оптимизацию, делающую вычисление некоторых аргументов функций ленивым. Оптимизация происходит на уровне LLVM IR в SSA форме.

Статья описывает два алгоритма:
1) модификацию Value Slicing, которая строит замыкание для вычисления произвольного значения в SSA IR.
2) Lazification по информации от профилировщика заменяет call-by-value на call-by-need.

Выбор вычислений для lazification и необходимое профилирование программы описаны в более ранней статье:
Chang, Stephen. "Laziness by need." Programming Languages and Systems: 22nd European Symposium on Programming, ESOP 2013

Авторы добились роста производительности программ на C и C++ от 1% до ~10% за счет увеличения размера на ~10%; тесты включают в себя SPEC CPU 2017.

#optimization #profiling #lazy #llvm
Новости PLComp №1, часть 1

📚Статья Flan: An expressive and efficient datalog compiler for program analysis от Supun Abeysinghe, Xhebraj Anxhelo и Tiark Rompf (Purdue)

Реализация компилятора Datalog, который на тестах опережает по производительности известный компилятор Soufflé. Реализация основана на eDSL, что позволяет расширять Datalog с помощью механизмов хост-языка Scala. Для eDSL написан простой интерпретатор, код которого, с помощью Scala LMS (первая проекция Футамуры), транслируется в производительный код на C.

📚Заметки Compiling With Constraints (Mar 18, 2024) от Филипа Цукера (Philip Zucker)

Заметки по очень перспективному подходу к использованию программирования в ограничениях для декларативного описания фаз выбора команд, планирования и распределения регистров. Во многом основано на изучении проекта Unison. Минимум теории, максимум практики в виде примеров кода на Python и MiniZinc.

📚Видео доклада Архитектурно-зависимые оптимизации компилятора LCC для микропроцессоров архитектуры Эльбрус (2024 г.), автор М.И. Нейман-Заде

Доклад посвящён работе оптимизирующего компилятора lcc для процессоров Эльбрус с широким командным словом (VLIW). Компилятор lcc сам по себе структурно схож с традиционными компиляторами, однако особенности целевой платформы накладывают серьезные отпечатки на существующие низкоуровневые оптимизации. В докладе раскрываются аппаратные особенности работы процессоров Эльбрус, их отличия от "традиционных" процессоров, после чего описываются некоторые аппаратно-зависимые оптимизации, характерные для Эльбрусов.

Среди оптимизаций рассказывается про:
* Слияние кода (if-conversion).
* Конвейеризацию циклов (software pipelining с аппаратной поддержкой).
* Динамический разрыв зависимостей.
* Предварительную подкачку данных (улучшенный аналог prefetch).

📚Курс Declarative Program Analysis and Optimization (Spring 2024) от Макса Уилси (Max Willsey, UC Berkley) (если ссылка не работает, попробуйте отключить VPN)

Учебный курс с очень интригующим названием, поскольку декларативный подход к реализации фаз компилятора все еще остается уделом почти исключительно академистов. В курсе речь идет о переписывании термов, стратегическом переписывании, синтезе и верификации систем переписывания, использовании Datalog, о E-графах и насыщении равенствами. Словом, это ультрасовременный университетский курс по специальным вопросам компиляции.

📚Статья Fixing a Bug in PyPy's Incremental GC (2024-03-26)

Починка каждого бага, связанного с GC, это сама по себе уже интересная история,
потому что такие баги обычно нетривиальны. К тому же баг, про который рассказывает автор, приводит к memory corruption и это еще больше усложняет отладку. Поиск стабильного воспроизведения, пошаговая интерактивная отладка в gdb/rr,
написание скриптов для GDB API для упрощения отладки GC и т.д.

(продолжение следует)

#news
Новости PLComp №1, часть 2

📚Статья Troubleshooting an Intermittent Failure in CI Tests on ARM64 (October 4, 2023)

Тесты для Kong Gateway время от времени падали с синтаксической ошибкой, причем происходило это только на ARM64 и только с включенным компилятором в OpenResty (использует LuaJIT). В ходе исследования бага авторы выяснили причину — некорректная кодогенерация для ARM64, и предложили исправление в основной репозиторий LuaJIT.

Можно отметить, что в обеих статьях использовали отладчик rr. rr позволяет записывать выполнение программы и потом детерминировано его воспроизводить, а также выполнять инструкции в обратном порядке.

📚Видео доклада How Badly Do We Want Correct Compilers? (NDC TechTown 2023)

Доклад известного исследователя Джона Регира (John Regehr). Докладчик, насколько можно заметить, ни разу не произносит слово "CompCert" и сомневается в успешности проектов формально верифицированных компиляторов для популярных ЯП, что, конечно, интригует. Рассматриваются инструменты, в разработке которых принял участие автор доклада: YARPGen, Souper и другие проекты.

📚Заметки Compiling ML models to C for fun (September 19, 2023) и Vectorizing ML models for fun (February 18, 2024), автор Макс Бернштейн (Max Bernstein)

Учебное пособие в двух частях для самых маленьких по разработке DL-компиляторов с примерами на Python. Автор начинает с реализации более производительного варианта библиотеки micrograd и создает компилятор, выдающий представление на C. Во второй части делается переход от скаляров к векторам и осуществляется автовекторизация с помощью выявления скалярных произведений в коде.

📚Видео доклада A Full Employment Theorem for PL Researchers: Domain-Specific Languages от Нейта Фостера (Nate Foster, Cornell) (23 Oct 2023)

Доклад призван воодушевить исследователей в области языков программирования на работу с современными DSL, в первую очередь с теми, которые используются для программирования специализированного "железа". Рассматривается Тьюринг-неполный язык P4 для разбора сетевых пакетов и ставится задача установления эквивалентности двух двоичных парсеров. Для решения задачи используется подход с использованием конечных автоматов, бисимуляции, SMT-решателя и Coq.

📚Видео доклада ægraphs: Acyclic E-graphs for Efficient Optimization in a Production Compiler, автор Chris Fallin (8 июля 2023 г.)

Перенацеливаемый генератор кода Cranelift является, судя по всему, первым промышленным проектом, где широко используются E-графы для решения задачи выбора порядка фаз при компиляции. В Cranelift, как показано в докладе, используется оригинальный упрощенный вариант E-графов -- ациклические E-графы (aegraphs). В предложенную схему на основе aegraphs включены и такие "тяжеловесные" оптимизации, как GVN и LICM.

Над выпуском работали: Пётр Советов, Сергей Бронников, Алексей Маркин

#news
📚 Пост AI-Powered Fuzzing: Breaking the Bug Hunting Barrier

В 2016 году Google запустила бесплатный сервис для фаззинга открытого ПО OSS-Fuzz. Это произошло после обнародования нескольких критических уязвимостей типа Heartbleed, которые, как обнаружилось, можно было предотвратить, если бы разработчики использовали фаззинг.

Чтобы использовать OSS-Fuzz, разработчик реализует фаззеры для своего проекта и конфигурирует взаимодействие с сервисом. Сервис запускает фаззеры, собирает результаты, приватно сообщает разработчикам о найденных багах, а затем следит за обновлениями чтобы удостовериться, что разработчики смогли исправить баг. Открытые проекты обслуживаются бесплатно, а для закрытого кода можно запустить сервис на своей машине.

В среднем, код проектов, использующих OSS-Fuzz, покрывается не более, чем на 30%, потому что писать фаззеры приходится вручную. Это и трудно, и долго, а разработчики не всегда могут выделить на эту задачу ресурсы. Чтобы помочь разработчикам увеличить покрытие кода фаззерами, в августе 2023 инженеры Google реализовали протокол интеграции OSS-Fuzz с LLM, дабы языковые модели генерировали фаззеры без участия человека.

OSS-Fuzz идентифицирует непокрытые части проекта и формирует запрос к LLM. Запрос включает информацию о непокрытой части проекта. Код фаззера может автоматически уточняться у языковой модели в случае ошибок компиляции или если сгенерированный фаззер не увеличил покрытие кода.

Примеры запросов к LLM и system prompts.

В описании репозитория проекта авторы приводят результаты одного из экспериментов:

- 1300+ бенчмарков из 300 проектов ПО.
- В 160 проектах LLM cумела сгенерировать корректные фаззеры и увеличить покрытие на 10-30%.

К сожалению, неясно, какую LLM использовали для этого эксперимента — заявлена поддержка Vertex AI code-bison и code-bison-32k, Gemini Pro и OpenAI GPT-3.5-turbo и GPT-4.

#llm #fuzzing
📚 Отчёт Wentao Gao, Van-Thuan Pham, Dongge Liu, Oliver Chang, Toby Murray, and Benjamin I.P. Rubinstein. 2023. Beyond the Coverage Plateau: A Comprehensive Study of Fuzz Blockers (Registered Report). In Proceedings of the 2nd International Fuzzing Workshop (FUZZING ’23), July 17, 2023


Отчёт анализирует некоторые причины проблемы плато, с которой сталкивается Coverage-Guided Greybox Fuzzing (CGF).

CGF это тип фаззинга, который характерен двумя особенностями. Во-первых, фаззер использует общую информацию об архитектуре программы для подбора значимых входов, но не знает её детальной структуры. Во-вторых, фаззер в процессе работы старается максимизировать покрытие кода трассами выполнения.

При CGF фаззер выполняет следующие шаги в цикле, пока не будет достигнут лимит времени:

1. Выбирает вход из накапливаемого множества входов (seed corpus).
2. Мутирует вход, пытаясь достигнуть непокрытых участков кода.
3. Запускает программу на мутированном входе. Если покрытие увеличилось, добавляет вход в seed corpus; при неожиданном поведении программы, например, ошибке времени выполнения, описывает этот случай в отчёте.

Алгоритмы CGF различаются правилами мутации, начальным заполнением seed corpus и другими деталями.

Эксперименты показывают, что современные фаззеры достаточно быстро выходят на плато, при котором покрытие кода перестает расти, сколько бы дополнительного времени ни получил фаззер.
Чтобы понять причины эффекта плато, авторы изучили результаты фаззинга трех популярных библиотек: libPNG, iGraph, OpenSSL с помощью утилиты FuzzIntrospector для cервиса OSS-Fuzz.
(Пример отчёта)

Фаззинг библиотек требует написания программ-оберток (drivers), которые запускают библиотечные функции. Этим занимаются, как правило, сами разработчики библиотек. По мнению авторов, чрезмерно упрощенные обертки являются важной причиной эффекта плато.

Анализ авторов показал, что проблемные ситуации часто возникали независимо от входов: в libPNG все 12 экспериментов показали плато на любых входах, а в iGraph — 9 из 22. Покрытие переставало увеличиваться по следующим причинам:

1. Аргументы функций в обертке зафиксированы. Например, библиотека iGraph поддерживает работу как с ориентированными, так и с неориентированными графами, но существующие обертки вызывали некоторые функции с такими фиксированными аргументами, что под фаззинг попадали только ориентированные графы.
2. Функции вообще не вызываются оберткой:
2.1. Вызываются не все перегрузки.
2.2. Ошибки бы проявились если бы функция была вызвана больше одного раза (например, init-функции).
2.3. Пропущены вызовы, которые могли бы настроить библиотеку на разные конфигурации.
2.4. Пропущены вызовы, добавляющие функциональность (например, установка обработчиков событий).
3. Функции вызываются только в "правильном", ожидаемом порядке, а ошибки проявляются лишь если перемешать вызовы.
4. Наличие объективно недостижимого кода.

Некоторые проблемы в обертке iGraph проявлялись только на определенных входах, которых фаззеры не достигали.
Это были крайние случаи — например, когда размер входного графа превышал предполагаемый лимит.
Неожиданно, но в эксперименте не возникло проблем, связанных с использованием контрольных сумм и магических чисел, хотя это известные проблемы для фаззинга.

Анализ фаззинга OpenSSL, видимо, будет включен в следующую работу.

В качестве решений проблем с обертками авторы предлагают рассмотреть автоматическую генерацию оберток и structure-aware fuzzing, имеющий полную информацию о коде программы.

#fuzzing
📚 Статья Rocha, Rodrigo CO, et al. "Hybf: A hybrid branch fusion strategy for code size reduction." Proceedings of the 32nd ACM SIGPLAN International Conference on Compiler Construction. 2023.

Авторы реализовали для LLVM IR два новых алгоритма слияния ветвей. 70% бенчмарков показали уменьшение размера кода до 4% поверх уже внедренных оптимизаций с пренебрежимо малым эффектом на производительность кода.

Распространенные алгоритмы слияния ветвей ограничены в применении:

- Первоначальная версия алгоритма слияния ветвей [1] работает только для похожих ветвей, содержащих по одному базовому блоку, т.е. для ромбов в графе потока управления.
- Другой алгоритм — Control Flow Melding (Divergence Aware Region Melder, DARM), поддерживает только структурно простые ветви: вложенные if-then, if-then-else или natural loops.

Авторы предлагают два новых алгоритма для слияния ветвей и алгоритм HyBF, выбирающий лучшую из двух трансформаций для конкретного подграфа потока управления. Они работают с т.н. регионами графа потока управления – это подграфы с одной точкой входа.

Первый алгоритм, предложенный авторами — Control Flow Melding (CFM-CS), это улучшенная версия алгоритма DARM.
CFM-CS работает только с SESE-регионами (single-entry-single-exit, регионы с одной точкой выхода).

Первоначально, DARM предназначен для борьбы с control flow divergence. Это ситуация в моделях вычислений Single Instruction Multiple Threads, когда несколько потоков выполняют один и тот же код, но выбирают разные пути из-за условного ветвления [2]. Если ветви выполняются неодинаковое время, потоки, уже выполнившие более короткую ветвь, ожидают еще не завершившиеся потоки, занятые длинными ветвями, как при барьерной синхронизации.
DARM обычно ведёт к уменьшению размера кода, но не нацелен на это, и не способен работать со сложно структурированными подграфами потока управления.

Улучшенная версия DARM — CFM-CS — работает следующим образом:

1. Определить SESE-регион, где возможно слияние (блок с двумя исходящими ветвями, сходящимися в одном блоке).
2. Собрать подрегионы каждой ветви в дерево (авторы используют структуру данных region tree в LLVM).
3. Изо всех возможных пар подрегионов, принадлежащих двум ветвям, нужно выбрать пару изоморфных подрегионов, причем с наиболее "похожими" инструкциями внутри. Для этого регионы должны быть выровнены, как в проблеме "выравнивания последовательностей"[3]. Авторы используют специальную эвристику в качестве меры сходства, позволяющей выбрать пары подрегионов наиболее выгодные для слияния.
4. Чтобы объединить изоморфные подрегионы из двух ветвей необходимо выровнять уже инструкции внутри соответствующих блоков. Это разделит инструкции блоков на:
- не имеющие соответствия в блоке изоморфного подрегиона. Эти инструкции перемещаются в новые, условно выполняемые базовые блоки.
- совпадающие с инструкциями в блоке изоморфного подрегиона. Это одинаковые инструкции с разными операндами: они будут помещены в объединенный базовый блок, но их операнды будут вычисляться с использованием псевдоинструкций SELECT. Условием таких SELECT'ов будет, разумеется, условие из входного блока большого SESE-региона.
Исходный код алгоритма CFM-CS для LLVM IR.

Алгоритм CFM-CS эффективен, когда ветви имеют похожую структуру, но если одна из ветвей содержит всего один блок, такой блок можно обернуть в код, имитирующий структуру другой ветви, а затем запустить на полученном регионе CFM-CS. Эта техника называется Region Replication, и авторы также предлагают улучшение существующего алгоритма Region Replication используемого в DARM.

Второй предложенный алгоритм SEME-fusion обобщает алгоритм слияния функций HyFM, предложенный авторами в предыдущей статье [4], на любой SEME-регион (Single-Entry-Multiple-Exit). Он сравнивает блоки двух областей SEME на основе расстояний между их fingerprint'ами, вычисляя linear pairwise alignment [4]. После выравнивания и слияния блоков происходит технический процесс корректировки ϕ-функций в выходных блоках.
Исходный код алгоритма SEME-fusion для LLVM IR.
Таким образом, CFM-CS лучше работает для ветвей с похожей структурой, а SEME-fusion предпочтителен в ситуациях SEME-регионов или для сильно различающихся по структуре ветвей.
Так как эти два подхода ортогональны, авторы реализовали специальный проход по модулям в LLVM, применяющий к регионам с ветвлением обе трансформации и выбирающий лучшую.

[1] B. Coutinho, D. Sampaio, F. M. Q. Pereira, and W. Meira Jr. 2011. Divergence Analysis and Optimizations. In 2011 International Conference on Parallel Architectures and Compilation Techniques. 320–329.
[2] Saumya, Charitha, Kirshanthan Sundararajah, and Milind Kulkarni. "DARM: Control-Flow Melding for SIMT Thread Divergence Reduction--Extended Version." arXiv preprint arXiv:2107.05681 (2021).
[3] https://en.wikipedia.org/wiki/Sequence_alignment
[4] Rocha, Rodrigo CO, et al. "HyFM: Function merging for free." Proceedings of the 22nd ACM SIGPLAN/SIGBED International Conference on Languages, Compilers, and Tools for Embedded Systems. 2021.


#llvm #function_merging #optimization #branch_merging #code_size_reduction
Implementation Strategies for Mutable Value Semantics
Dimitri Racordon, Denys Shabalin, Daniel Zheng, Dave Abrahams, Brennan Saeta
https://kyouko-taiga.github.io/assets/papers/jot2022-mvs.pdf

Одно из неоспоримых достоинств языка Rust — он наглядно показал, что безопасная работа с указателями и ссылками сложна, и требует следовать довольно замысловатым правилам, уберегающим от неочевидных потенциальных проблем. Даже опыт более простых языков, повсеместно использующих ссылки и GC — например, Java — показывает, что непреднамеренный алиасинг (aliasing) может создавать дыры в системе безопасности, помимо других проблем.

Эти два фактора — сложность системы типов и владения в Rust, и сложность безопасного использования ссылок без контроля алиасинга — привели к появлению ряда языков, например, https://www.hylo-lang.org/ или https://vale.dev/, отказавшихся от первоклассных ссылок вообще. Отказ от первоклассных ссылок означает, что в языке просто нет механизмов, позволяющих явным образом взять ссылку на объект в памяти, передать её в функцию или того хуже — записать в поле другого объекта.

Отказавшись от ссылок и ссылочной семантики, мы приходим к семантике значений. Концептуально, каждое присваивание копирует значение целиком из одного места в другое, а каждый объект владеет своими частями (whole-part relation). Благодаря этому все данные в программе образуют лес, корни деревьев которого располагаются в переменных. И поскольку в такой структуре не может быть алиасинга, можно безбоязненно сделать значения изменяемыми, откуда и возникает Mutable Value Semantics — семантика изменяемых значений. Возможность изменения "по месту" (in-place) частей объектов и отличает MVS от семантики неизменных (immutable) значений в функциональном программировании, которая тривиально реализуется поверх ссылок на общие части в персистентных структурах данных.

Естественно, что наивная реализация такой семантики крайне неэффективна из-за постоянного копирования развесистых структур данных. Самое очевидное узкое место — работа с массивами: при передаче массивов в функции их пришлось бы копировать, а единственный способ вернуть массив из функции — снова создать копию. Для обхода этой проблемы и реализации работы с массивами и другими структурами "по месту" (in-place) предлагается использовать ключевое слово inout для параметров функций, передаваемых "по ссылке". Это и есть "ссылки второго класса", которые возникают и используются неявно, без возможности сохранения в переменных или полях объектов.

Тем не менее, поскольку по ссылке могут передаваться только аргументы функций, а всё остальное должно копироваться — это всё ещё непозволительно много копирований. Особенно "лишних" копирований значений, которые никогда и не меняются, что значит для них алиасинг не наблюдаем и не создаёт проблем (аналогично ФП, упоминавшемуся выше). Чтобы избавиться от этих копий предлагается использовать механизмы Copy-on-Write и подсчёт ссылок при реализации языка. Для уменьшения инкрементов и декрементов счётчика предлагается обратиться к таким работам как Perceus для Koka и Lean 4.

Однако возникает вопрос: коль скоро мы вводим неявные ссылки в наш язык и его реализацию, не сломают ли они семантику изменяемых значений? Чтобы на него ответить, авторы формализуют core language, его систему типов и операционную семантику, после чего доказывают корректность оптимизаций и сохранение семантики изменяемых значений теми правилами, которым подчиняется работа inout аргументов.

Но авторы не ограничились теоретическим рассмотрением вопроса, а пошли дальше, и реализовали прототип предлагаемой семантики с оптимизациями в виде упрощённого языка программирования на основе Swift, используя LLVM в качестве бэкенда. Это позволило оценить производительность прототипа в сравнении с другими языками, использующими LLVM (C++, Swift, Scala Native) на ряде синтетических бенчмарков и общеизвестных примеров (Mandelbrot, NBody, NQueens, etc.).
Бенчмарки показали жизнеспособность подхода MVS — производительность оказалась на уровне между Scala Native и Swift. Отставание от Swift авторы объясняют тем, что в прототипе не использовались более продвинутые оптимизации, в частности, уменьшающие количество обращений к счётчику ссылок. Таким образом подход имеет перспективы значительного дальнейшего улучшения за счёт комбинирования с другими известными оптимизациями (например, упоминавшийся Perceus).

C++ в бенчмарках использовался в качестве реализации наивного подхода "копируем всё и всегда, но с агрессивными компиляторными оптимизациями", поэтому ожидаемо показал худшую производительность, кроме тестов с преобладающим количеством изменений "по месту" с передачей аргументов через inout параметры.

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

#mutable_value_semantics #swift #CoW
Возможно вы знакомы с алгоритмами сборки мусора, которые используются в языках программирования. Американский программист Кен Фокс (Ken Fox) написал программу с использованием разных алгоритмов сборки мусора и вариант программы без освобождения памяти, собрал данные для каждого варианта запуска программы и сгенерировал анимированные изображения для них. В репозитории представлены визуализации четырех основных алгоритмов сборки мусора:

- Mark-sweep
- Mark-compact
- Copying
- Reference counting

Каждому из этих алгоритмов посвящена отдельная глава в основополагающем учебнике The Garbage Collection Handbook. Рекомендуем обратиться к этому учебнику для полного погружения в тему.

Каждое анимированное изображение - это пространство памяти, выделенное процессу. Чёрным цветом изображена неиспользуемая память, по мере использования память начинает подсвечиваться жёлтым (операции записи) и зелёным (операции чтения) цветами. Цвет фрагментов памяти, которые используются дольше, начинает тускнеть, чтобы визуализировать развитие процесса во времени. По ходу выполнения программа перестаёт использовать отдельные участки памяти. Они и считаются "мусором".

Первая анимация демонстирует отсутствие всякой сборки мусора - NO_GC, в этом случае память освобождается только при завершении процесса. Механизм NO_GC прост в реализации, он удобен, когда есть способ разбить задачу на отдельные подпроцессы. Так, например, работает веб-сервер Apache.

Вторая анимация демонстрирует работу сборщика мусора с использованием техники подсчёта ссылок. Суть алгоритма заключается в подсчёте количества ссылок на объекты в памяти, если количество ссылок становится равным нулю, то объект удаляется. Подсчёт ссылок - единственный алгоритм, хорошо совместимый с разными менеджерами ресурсов. По сравнению с первой анимацией появились красные пиксели, они соответствуют операциям подсчёта ссылок. Можно оценить и эффективность сборщика, если сразу после красной вспышки область памяти освобождается.

Третья анимация демонстрирует работу алгоритма "mark-and-sweep". Все объекты делятся на достижимые и недостижимые. Определённое множество объектов считается достижимым изначально - так называемые корневые объекты. Объект, на который есть ссылка из достижимого объекта, тоже считается достижимым. При запуске сборщик мусора выполняет следующие шаги:

- Фаза Mark: отмечает все достижимые объекты
- Фаза Sweep: проходит рекурсивно по объектам в куче, удаляет недостижимые объекты и возвращает их в пул свободной памяти.

Визуализации остальных алгоритмов приведены в репозитории. Помимо описанных визуализаций алгоритмов сборки мусора есть и другие, например интерактивная визуализация алгоритма tricolor mark-sweep, используемого в реализации языка Go.
This media is not supported in your browser
VIEW IN TELEGRAM
Программа без освобождения памяти (NO_GC).
This media is not supported in your browser
VIEW IN TELEGRAM
Программа со счётчиком ссылок.
This media is not supported in your browser
VIEW IN TELEGRAM
Программа с использованием сборщика мусора с алгоритмом Mark and Sweep.
Forwarded from Covalue
Bertot, [2008] "Structural abstract interpretation, A formal study using Coq"

+ порт на Агду

В статье обсуждается кодирование внутри теории типов подхода к статическому анализу программ, известного как абстрактная интерпретация (АИ). Для анализа взят простой императивный язык с целочисленными значениями (можно ограничить до натуральных) со сложением и булевым сравнением, мутабельными переменными, последовательной композицией и while-циклами. Корректность полученного абстрактного интерпретатора доказана относительно аксиоматической (Хоаровой) семантики языка, а гарантия завершимости получается по построению.

Идея абстрактной интерпретации заключается в вычислении приближенных значений для некоторых аспектов поведения программы - обычно анализ делается по графу потока управления программы, но в статье анализируется напрямую синтаксическое дерево. Такую аппроксимацию, как правило, можно получить значительно быстрее, нежели исполнять саму программу, при этом на её основании можно проводить компиляторные оптимизации и делать выводы о корректности. Стандартным примером приближенного анализа является огрубление точных значений числовых переменных до бита четности или интервала значений. Классически при этом выводится аппроксимация "сверху" (overapproximation), т.е. мы хотим гарантировать отсутствие ложноотрицательных результатов за счет возможных ложноположительных (стандартный компромисс статического анализа). Формально отношения между точными (конкретными) и приближенными (абстрактными) множествами значений описываются в теории АИ через соответствия Галуа (пара функции абстракции α и функции конкретизации γ, необходимы для транспорта отношений порядка), в статье используется ad-hoc форма этой конструкции. Также требуется, чтобы домен абстрактных значений имел структуры полугруппы (т.к. моделируются числовые значения) и ограниченной (полу)решетки.

Для задания семантики анализируемого языка используется исчисление предусловий, состоящее из (1) бескванторных логических формул (предусловий/assertions) и аннотированных ими программ, (2) функции вычисления наислабейшего предусловия (формулы, которой должны удовлетворять переменные в начале исполнения) по аннотированной программе и (3) функции для генерации условий проверки (набор импликаций, которые, будучи валидными, гарантируют что любое исполнение программы, стартующее из удовлетворяющего предусловию состояния, будет удовлетворять всем последующим формулам). Для while-циклов также добавляются явные инварианты. Нужно заметить, что в самой статье полноценная аксиоматическая семантика c пред- и постусловиями не вводится, но это проделано в предыдущей статье автора, откуда я взял соответствующий кусок с доказательствами корректности аксиоматической семантики относительно операционной и корректности генератора условий. Ключевым же свойством генератора условий для корректности абстрактного интерпретатора является монотонность, т.е. валидность условий, построенных из некой формулы влечет валидность условий, построенных из её ослабления. Задача абстрактного интерпретатора - по исходной программе и абстрактному начальному состоянию одновременно вывести аннотированную программу и абстрактное конечное состояние.

В статье демонстрируется построение двух вариантов абстрактного интерпретатора, причем во втором, более продвинутом, сделана попытка обнаруживать незавершимость и мёртвые ветки кода. В качестве базовой инфраструктуры для обоих вариантов вводится таблица поиска (lookup table) для абстрактных состояний переменных (пары из имени переменной и абстрактного значения; отсутствующие переменные имеют по умолчанию значение верхнего элемента полурешетки) и набор функций для отображения этих состояний в логические формулы, каковые функции должны иметь ряд свойств (включающие, например, требования параметричности отображения и перевода верхнего элемента в тривиальную истину). Для верификации интерпретатора также нужна функция построения предиката внутри метатеории (теории типов) по его сигнатуре.
Forwarded from Covalue
Первый интерпретатор просто использует эти функции напрямую для рекурсивного аннотирования программы, не пытаясь вывести инварианты циклов (подставляя вместо них тривиально истинные формулы). Его корректность сводится к трем свойствам: (1) наислабейшее предусловие, полученное из вычисленного конечного состояния, действительно слабее предусловия, полученного из начального состояния, (2) все условия проверки, полученные из вычисленной аннотированной программы и конечного состояния, являются валидными и (3) вычисленная аннотированная программа идентична исходной программе после стирания аннотаций. Пример работы этого интерпретатора проиллюстрирован на домене битов четности со свободно добавленным верхним элементом (кодирующим отсутствие информации).

Второй интерпретатор более серьезно подходит к работе с циклами, используя информацию о том, что тест цикла всегда истинен внутри его тела и ложен на выходе. Незавершимость кодируется обертыванием конечного состояния в опциональный тип в выдаче интерпретатора. К параметрам добавляются две новые функции, которые вычисляют уточнения абстрактного состояния в зависимости от успешно и неуспешно пройденного теста цикла соответственно; именно эти функции берут на себя основную сложность задачи, в частности, они пытаются выводить информацию о незавершимости, и эта же информация позволяет пометить мертвый код тривиально ложными аннотациями. Схема процесса вывода инварианта цикла (в общем случае эта задача неразрешима!) выглядит так: (1) несколько абстрактных прогонов тела цикла (их количество контролируется эвристикой) с нарастающим расширением множества значений переменных - если процесс стабилизировался, инвариант найден; (2) если инвариант не найден, запускаем процесс 1 еще раз с переаппроксимацией для значений некоторых переменных - это позволяет ускорить процесс схождения (количество этих повторов также контролируется отдельной эвристикой); (3) если инвариант всё еще не найден, выбрать гарантированный инвариант (первый интерпретатор сразу же делает это, используя тривиальную истину); (4) сужение переаппроксимированных инвариантов повторным запуском интерпретатора (наличие этого шага приводит к определению интерпретатора через вложенную рекурсию).

Реализация этих шагов требует введения ряда вспомогательных функций-параметров, например, для сравнения количества информации в двух абстрактных значениях. Для доказательства корректности второго интерпретатора (состоящего из тех же трёх частей, что и в первом случае) также требуются 14 новых условий, описывающих все эти новые функции и их взаимодействие. Обе реализации интерпретаторов и их доказательства упакованы в модули, параметризующие их по абстрактному домену. Первый интерпретатор уже был ранее инстанцирован доменом чётности, а для второго описано кодирование домена числовых интервалов с потенциально бесконечными границами, где роль верхнего элемента играет интервал (-∞,+∞), а переаппроксимация заключается в замене одной из границ на -/+∞.

В заключение автор отмечает, что статью не следует рассматривать как полноценное введение в теорию АИ, а реализованные интерпретаторы являются моделями, т.к. оставляют желать лучшего в плане производительности - например, при вычислении аннотаций повторяется работа, уже проделанная ранее при поиске инварианта цикла. Кроме того, статический анализ может быть эффективнее, если рассматривать не отдельные переменные, а отношения между несколькими переменными.

#automatedreasoning
Я и мой студент, Кирилл Павлов, опубликовали статью "Библиотека llvm2py для анализа промежуточного представления LLVM и её применение в оценке степени распараллеливания линейных участков кода".

Чем может быть интересна работа студента второго курса бакалавриата? Анализ распараллеливания делается для варианта LLVM IR, где линейные участки заданы ярусно-параллельной формой (ЯПФ). Автоматическое построение ЯПФ позволяет узнать минимальное число шагов, за которое неограниченно параллельный LLVM-процессор может выполнить код линейного участка.

В реальных процессорах число параллельно работающих вычислительных элементов конечно. Поэтому важно экономить ресурсы: получить минимальную ширину ЯПФ, при которой достигается минимальное число шагов параллельной LLVM-машины. Так можно предварительно оценить выбор того или иного аппаратного ускорителя или же узнать, какой "шириной" должен обладать проектируемый нами ускоритель. В статье эта задача сводится к задаче программирования в ограничениях и эффективно решается с помощью инструментария Google OR-Tools.

P.S. Сама библиотека llvm2py, предназначенная для создания такого рода статических анализаторов кода LLVM IR, была полностью реализована Кириллом.

#llvm #analysis #solver
2024/11/22 23:14:24
Back to Top
HTML Embed Code: