Библиотека сайта rus-linux.net
Компилятор Glasgow Haskell
Глава 5 из книги "Архитектура приложений с открытым исходным кодом", том 2.Оригинал: The Glasgow Haskell Compiler
Авторы: Simon Marlow и Simon Peyton-Jones
Перевод: Н.Ромоданов
Разработка системы RTS
Система времени выполнения компилятора GHC представляет собой во многих отношениях разительный контраст с самим компилятором. Есть естественные различия, поскольку система времени выполнения написана на языке С, а не на языке Haskell, но есть также и соображения, уникальные для системы RTS, которые ведут к другой философии проектирования:
- Каждая программа на языке Haskell тратит много времени на выполнение кода в системе RTS: обычно это около 20 - 30%, но характеристики программ на языке Haskell очень сильно варьируются, поэтому также обычными бывают и значения, выходящие из этого диапазона как в большую, так и в меньшую сторону. Если система RTS оптимизирована, то выгоды возрастают многократно, так имеет смысл потратить много времени и усилий на оптимизацию.
- Система времени выполнения статически компонуется с каждой программой на языке Haskell (в случаях, когда не используется динамическая компоновка), так что есть стимул сделать эту систему поменьше.
- Ошибки времени выполнения часто непонятны пользователю (например, «ошибка сегментации») и их трудно обойти. Например, ошибки в сборщике мусора, как правило, не привязаны к использованию конкретной особенности языка, но они возникают, когда во время выполнения возникают некоторые сложные комбинации факторов. Кроме того, ошибки такого рода, как правило, являются непостоянными (случаются только при некоторых запусках программы) и они очень чувствительны к изменениям (крошечные изменения в программе приводят к исчезновению ошибки). С ошибками в многопоточной версии среды выполнения возникает еще больше проблем. Поэтому есть смысл прилагать дополнительные усилия для того, чтобы не допускать таких ошибок, а также для того, чтобы создать инфраструктуру, в которой такие ошибки будут легче идентифицироваться.
Симптомы ошибок системы RTS часто неотличимы от двух других видов отказов: сбоя оборудования, которые происходят гораздо чаще, чем вы думаете, и неправильного использования небезопасных возможностей языка Haskell, например, интерфейса FFI (Foreign Function Interface — интерфейса внешних функций). Первое, что нужно сделать при диагностике отказов времени выполнения, это исключить эти две указанные причины.
- Система RTS является низкоуровневым кодом, который работает на машинах с несколькими различными вариантами архитектуры и с различными операционными системами, и она регулярно портируется на новые машины. Возможность портирования имеет важное значение.
Важным является каждый цикл и каждый байт, и, тем более, их правильность. Кроме того, задачи, выполняемые системой времени выполнения, сложны по своей природе, поэтому с самого начала трудно добиться их правильного выполнения. Согласование всех этих особенностей привело нас к некоторым интересным оборонительным приемам, о которых мы расскажем в следующих разделах.
Борьба со сложностью
Система RTS является сложной средой, которая враждебна программированию. В отличие от компилятора, система RTS практически не является типобезопасной. На самом деле, она еще менее типобезопасная, чем большинство других программ на языке C, т.к. она управляет структурами данных, типизация которых поддерживается на уровне языка Haskell, а не на уровне языка C. Например, в системе RTS не известно, что объект, на который ссылается последняя ячейка, является массивом или другим объектом: эта информация просто не существует на уровне языка C. Более того, процесс компиляции кода Haskell стирает информацию о типах, поэтому даже если мы бы сообщили системе RTS о том, что объект, на который происходит ссылка, является списком, все еще бы не было информации о голове списка. Поэтому в коде системы RTS требуется выполнять большое количество преобразований типов указателей языка C, причем с точки зрения безопасности типов это будет для компилятора C очень слабой подмогой.
Поэтому наше первое оружие в этой битве — избегать добавлений кода в систему RTS. Всегда, когда это можно, мы помещаем в систему RTS минимальное количество функций, а остальное пишем в библиотеке на языке Haskell. Это редко когда ухудшает результат; код на языке Haskell гораздо более надежен и белее компактен, чем на языке C, а его производительность, как правило, вполне приемлема. Точно нельзя сказать, где нужно проводить границу, хотя во многих случаях это достаточно ясно. Например, несмотря на то, что на языке Haskell теоретически можно реализовать сборку мусора, на практике это сделать чрезвычайно трудно, поскольку язык Haskell не позволяет программисту точно контролировать распределение памяти и т.п., поэтому на практике имеет смысл для такой низкоуровневой задачи перейти на уровень языка C.
Есть много функциональных возможностей, которые не удается (легко) реализовать на языке Haskell, а писать их в системе RTS не хочется. В следующем разделе мы сосредоточимся на одном аспекте управления сложностью и корректностью в системе RTS: на использовании инвариантов.
Инварианты и их проверка
В системе RTS есть много инвариантов. Многие из них являются тривиальными и легко проверяются: например, если указатель на голову очереди равен NULL
, то указатель на хвост очереди также должен быть равным NULL
. В коде системы RTS есть масса мест, где можно задать утверждения (assertion) для проверки аналогичных условий. Проверка утверждения является способом обнаружения ошибок раньше, чем они могут проявиться; на самом деле, когда добавляется новый инвариант, мы прежде чем писать код, реализующий инвариант, часто сначала добавляем утверждение.
Некоторые из инвариантов выявлять и проверять в процессе выполнения программ достаточно трудно. Один инвариантов этого вида, который больше относится к системе RTS, состоит в следующем: в куче не должно быть «висячих» указателей.
Висячие указатели добавить достаточно просто, и, как в компиляторе, так и в системе RTS, есть много мест, где может быть нарушен этот инвариант. Генератор кода может сгенерировать код, в котором создаются недопустимые объекты кучи; сборщик мусора, когда он сканирует кучу, может забыть обновить указатели некоторого объекта. На отслеживание ошибок этого вида может потребоваться очень много времени (однако, это является одним из любимых занятий автора статьи!), поскольку к тому времени, когда с программой, в конечном итоге, произойдет авария, процесс выполнения программы может пройти достаточно длинный путь от того места, где первоначально был добавлен висячий указатель. Есть хорошие средства отладки, но они не так хороши при прокрутке программы в направлении, обратном ходу ее исполнения. Однако, в последних версиях отладчика GDB и отладчика Microsoft Visual Studio действительно есть некоторая поддержка прокрутки программы в обратном направлении.
Общий принцип следующий: «если в программе дело идет к аварии, то это должно происходить, насколько это возможно, как можно быстрее, с большим количеством сообщений, которые должны выдаваться как можно чаще». Эта цитата взята из правил кодирования компилятора GHC, первоначально написанными Аластером Рейдом (Alastair Reid), который работал над ранней версией системы RTS.
Проблема в том, что не всегда инвариант отсутствия висячих ссылок можно проверить с помощью утверждения, требующего константного времени. Утверждение, с помощью которого делается эта проверка, должно сделать полный обход кучи! Очевидно, мы не сможем использовать это утверждение каждый раз, когда из кучи выделяется память, или каждый раз, когда сборщик мусора сканирует объект. На самом деле, даже этого было бы недостаточно, поскольку висячие указатели не появятся до тех пор, пока память не будет освобождена после окончания работы сборщика мусора.
Поэтому в отладочном варианте системы RTS есть дополнительный режим, который мы называем проверкой работоспособности (sanity checking). В процессе проверка работоспособности разрешается использовать все виды ресурсоемких утверждений и мы можем выполнять программу во много раз медленнее. В частности, в процессе проверки работоспособности проходит полное сканирование кучи с целью (среди всего прочего) проверить наличие висячих указателей до и после каждого обращения к сборщику мусора. Первое, что нужно сделать для того, чтобы выяснить причину аварии времени выполнения, это запустить программу с включенным режимом проверки работоспособности; иногда это позволит обнаружить нарушение инварианта задолго до того момента, когда программа фактически выходит из строя.
5.7. Заключение
За последние 20 лет авторы настоящей статьи вложили значительную часть своей жизни в проект GHC и мы весьма гордимся тем, как далеко он продвинулся. Это не единственная реализация языка Haskell, но это единственная реализация, которой регулярно пользуются сотни тысяч людей для того, чтобы выполнять реальную работу. Мы постоянно удивляемся, когда язык Haskell начинают использовать в необычных местах; одним из последних примеров является использование языка Haskell для управления системами в грузовиках-мусоровозах.
Для многих язык Haskell и компилятор GHC являются синонимами: эта цель никогда не планировалась, да и во многих случаях было бы контрпродуктивно иметь только одну реализацию стандарта, но дело в том, что поддержка хорошей реализации языка программирования требует большого объема работы. Мы надеемся, что наши усилия, затраченные в проекте GHC на поддержку стандарта и четкого выделения каждого отдельного расширения языка, сделают возможным появление других реализаций и их интеграцию с системой пакетов и другой инфраструктурой. Конкуренция выгодна для всех!
Мы выражаем глубокую благодарность фирме Microsoft, в частности, за предоставленную нам возможность разрабатывать компилятор GHC как часть наших исследований и распространять его с открытым исходным кодом.
Вернуться к началу статьи.