Это хорошая идея, чтобы скомпилировать язык для C? - PullRequest
42 голосов
/ 23 января 2012

По всему Интернету у меня возникает ощущение, что написание C-бэкенда для компилятора больше не является хорошей идеей.C-бэкэнд GHC больше не разрабатывается (это мое неподдерживаемое чувство).Компиляторы нацелены на C-- или LLVM.

Обычно я думаю, что GCC - это старый добрый зрелый компилятор, который хорошо работает при оптимизации кода, поэтому при компиляции в C будет использоваться зрелость GCC, чтобы получить лучший иболее быстрый кодРазве это не правда?

Я понимаю, что вопрос в значительной степени зависит от природы компилируемого языка и других факторов, таких как получение более поддерживаемого кода.Я ищу более общий ответ (относительно скомпилированного языка), который фокусируется исключительно на производительности (без учета качества кода и т. Д.).Я был бы также очень рад, если бы в ответ входило объяснение того, почему GHC отдаляется от C и почему LLVM работает лучше в качестве бэкэнда ( см. ) или любых других примеров компиляторов, выполняющих то же, что и яне в курсе.

Ответы [ 10 ]

27 голосов
/ 24 января 2012

Позвольте мне перечислить две мои самые большие проблемы с компиляцией в C. Если это проблема для вашего языка, зависит от того, какие функции у вас есть.

  • Сборка мусора Когда у вас есть сборка мусора, вам может потребоваться прервать обычное выполнение практически в любой точке программы, и в этот момент вам нужно получить доступ ко всем указателям, которые указывают в кучу , Если вы компилируете в C, вы не знаете, где могут быть эти указатели. C отвечает за локальные переменные, аргументы и т. Д. Указатели, вероятно, находятся в стеке (или, возможно, в других окнах регистров SPARC), но реального доступа к стеку нет. И даже если вы сканируете стек, какие значения являются указателями? LLVM фактически решает эту проблему (хотя я не знаю, насколько хорошо, поскольку я никогда не использовал LLVM с GC).

  • Хвостовые вызовы Многие языки предполагают, что хвостовые вызовы работают (т. Е. Они не увеличивают стек); Схема обязывает его, Хаскелл принимает это. Это не относится к C. При определенных обстоятельствах вы можете убедить некоторые компиляторы C выполнить хвостовые вызовы. Но вы хотите, чтобы хвостовые вызовы были надежными, например, когда хвост вызывает неизвестную функцию. Есть неуклюжие обходные пути, такие как прыжки на батуте, но ничего вполне удовлетворительного.

23 голосов
/ 23 января 2012

Хотя я не эксперт по компилятору, я считаю, что все сводится к тому, что вы теряете что-то при переводе на C, а не на перевод, например, на. Промежуточный язык LLVM.

Если вы думаете о процессе компиляции в C, вы создаете компилятор, который переводит в код C, затем компилятор C преобразует в промежуточное представление (AST в памяти), а затем переводит это в машинный код. Создатели компилятора C, вероятно, потратили много времени на оптимизацию определенных созданных человеком шаблонов в языке, но вы вряд ли сможете создать достаточно модный компилятор из исходного языка для C, чтобы подражать тому, как люди пишут код. При переходе к C теряется верность - компилятор C не знает о структуре вашего исходного кода. Чтобы добиться этих оптимизаций, вы по сути настраиваете свой компилятор, чтобы попытаться сгенерировать код C, который компилятор C знает, как оптимизировать при построении своего AST. Грязное.

Если, однако, вы переводите напрямую на промежуточный язык LLVM, это похоже на компиляцию кода в независимый от машины высокоуровневый байт-код, который похож на компилятор C, предоставляющий вам доступ для точного указания того, что должен содержать его AST. По сути, вы исключаете посредника, который анализирует код C, и переходите непосредственно к высокоуровневому представлению, которое сохраняет больше характеристик вашего кода, требуя меньшего количества переводов.

Также связанный с производительностью, LLVM может сделать некоторые действительно сложные вещи для динамических языков, такие как генерация двоичного кода во время выполнения. Это «классная» часть компиляции точно в срок: она пишет двоичный код, который будет выполняться во время выполнения, вместо того, чтобы застрять в том, что было создано во время компиляции.

8 голосов
/ 24 января 2012

Как вы упомянули, хороший ли C целевой язык очень сильно зависит от вашего исходного языка.Вот несколько причин, по которым С имеет недостатки по сравнению с LLVM или пользовательским целевым языком:

  • Сборка мусора : язык, который хочет поддерживать эффективную сборку мусора, должензнать дополнительную информацию, которая мешает C. Если распределение не удается, GC должен найти, какие значения в стеке и в регистрах являются указателями, а какие нет.Поскольку распределитель регистров не находится под нашим контролем, нам нужно использовать более дорогие методы, такие как запись всех указателей в отдельный стек.Это лишь одна из многих проблем, возникающих при попытке поддержать современный GC поверх C. (Обратите внимание, что LLVM также все еще имеет некоторые проблемы в этой области, но я слышал, что над этим работают.)

  • Отображение функций и оптимизация для конкретного языка : Некоторые языки полагаются на определенные оптимизации, например, Схема полагается на оптимизацию хвостового вызова.Современные компиляторы Си могут сделать это, но не гарантировано, что это может вызвать проблемы, если программа полагается на это для корректности.Другая функция, которую может быть сложно поддерживать поверх Си, - это сопрограммы.

    Большинство динамически типизированных языков также не могут быть хорошо оптимизированы компиляторами Си.Например, Cython компилирует Python в C, но сгенерированный C использует вызовы многих универсальных функций, которые вряд ли будут хорошо оптимизированы даже в последних версиях GCC.Своевременная компиляция, в том числе PyPy / LuaJIT / TraceMonkey / V8, намного лучше подходит для обеспечения высокой производительности динамических языков (за счет гораздо больших усилий по реализации).

  • Опыт разработки : Наличие интерпретатора или JIT также может дать вам гораздо более удобный опыт для разработчиков - создание кода на С, его компиляция и связывание, безусловно, будут медленнее и менее удобными.

Тем не менее, я все еще думаю, что разумно выбрать использование C в качестве цели компиляции для создания прототипов новых языков.Учитывая, что LLVM был явно задуман как бэкэнд компилятора, я бы рассматривал C, только если есть веские причины не использовать LLVM.Если исходный язык очень высокого уровня, тем не менее, вам, скорее всего, потребуется более ранний этап оптимизации более высокого уровня, так как LLVM действительно очень низкого уровня (например, GHC выполняет большую часть своих интересных оптимизаций перед генерацией вызова в LLVM).Да, и если вы создаете прототип языка, возможно, проще всего использовать интерпретатор - просто старайтесь избегать функций, которые слишком сильно зависят от реализации интерпретатором.

8 голосов
/ 23 января 2012

Одной из причин того, что GHC отошел от старого бэкэнда C, было то, что код, созданный GHC, не был тем кодом, который gcc мог особенно хорошо оптимизировать.Таким образом, с улучшением работы собственного генератора кода в GHC стало меньше отдачи от большой работы.Начиная с 6.12, код NCG был только медленнее, чем код, скомпилированный на С, в очень немногих случаях, поэтому с улучшением NCG в ghc-7 не было достаточного стимула для поддержки бэкэнда gcc.LLVM - лучшая цель, потому что она более модульная, и можно сделать много оптимизаций для ее промежуточного представления, прежде чем передавать результат в него.

С другой стороны, в последний раз, как я посмотрел, JHC все еще генерировал C и конечный двоичный файлИсходя из этого, как правило (исключительно?) GCC.И двоичные файлы JHC, как правило, бывают довольно быстрыми.

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

7 голосов
/ 24 января 2012

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

  1. Бесплатные компиляторы C (gcc, clang) немного ориентированы на Unix
  2. Поддержка более одного компилятора (например, gcc в Unix и MSVC в Windows) требует дублирования усилий.
  3. компиляторы могут перетаскивать библиотеки времени выполнения (или даже * nix-эмуляции) в Windows, которые являются болезненными. Две различные среды выполнения C (например, linux libc и msvcrt), на которых основываются, усложняют вашу собственную среду выполнения и ее обслуживание
  4. В вашем проекте есть большой внешний блоб с внешней версией, что означает, что основной переход на версию (например, изменение искажения может повредить вашу библиотеку времени выполнения, изменения ABI, такие как изменение выравнивания), могут потребовать некоторой работы. Обратите внимание, что это относится к компилятору и внешней версии (части) библиотеки времени выполнения. И несколько компиляторов умножают это. Это не так плохо для C, как для бэкэнда, как в случае, когда вы напрямую подключаетесь (читай: делайте ставки) к бэкенду, как если бы вы были gcc / llvm frontend.
  5. Во многих языках, которые следуют по этому пути, вы видите, что Цизмы проникают в основной язык. Конечно, вам это не понравится, но вы будете испытывать искушение: -)
  6. Функциональность языка, которая напрямую не сопоставляется со стандартным C (например, вложенные процедуры, и другие вещи, требующие многократного использования стека), сложны.
  7. Если что-то не так, пользователи будут сталкиваться с ошибками компилятора уровня C или компоновщика, которые находятся за пределами их сферы деятельности. Анализировать их и делать их своими собственными болезненными, особенно с несколькими компиляторами и -versions

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

Короче говоря, из того, что я видел, такой шаг позволяет быстро начать работу (получить разумный генератор кода бесплатно для многих архитектур), но есть и недостатки. Большинство из них связаны с потерей контроля и плохой поддержкой Windows таких * nix-ориентированных проектов, как gcc. (LLVM слишком нов, чтобы говорить о долгосрочной перспективе, но их риторика звучит так же, как и gcc десять лет назад). Если проект, от которого вы сильно зависите, придерживается определенного курса (например, GCC будет очень медленно работать на win64), то вы застряли с ним.

Во-первых, решите, хотите ли вы иметь серьезную поддержку не * nix (OS X более unixy), или только компилятор Linux с пробкой Mingw для Windows? Многим компиляторам нужна первоклассная поддержка Windows.

Во-вторых, насколько готовым должен стать продукт? Какова основная аудитория? Является ли это инструментом для разработчика с открытым исходным кодом, который может обрабатывать инструментальную цепочку DIY, или вы хотите нацелиться на начинающий рынок (как и многие сторонние продукты, например RealBasic)?

Или вы действительно хотите предоставить хорошо продуманный продукт для профессионалов с глубокой интеграцией и полным набором инструментов?

Все три являются действительными указаниями для проекта компилятора. Спросите себя, каково ваше основное направление, и не думайте, что больше вариантов будет доступно вовремя. Например. оцените, где находятся проекты, которые в начале девяностых годов выбрали в качестве внешнего интерфейса GCC.

По сути, Unix-путь заключается в расширении (максимизировать платформы)

Полные комплекты (такие как VS и Delphi, последний, который недавно также начал поддерживать OS X и в прошлом поддерживал linux) углубляются и пытаются максимизировать производительность. (специально поддерживает платформу Windows с глубокими уровнями интеграции)

Сторонние проекты менее понятны. Они больше идут за самозанятыми программистами и нишевыми магазинами У них меньше ресурсов для разработчиков, но они лучше управляют и фокусируются.

6 голосов
/ 23 января 2012

Один момент, который еще не затронут, - насколько близок ваш язык к Си? Если вы компилируете достаточно низкоуровневый императивный язык, семантика C может очень точно соответствовать языку, который вы реализуете. Если это так, то это, вероятно, победа, потому что код, написанный на вашем языке, вероятно, будет напоминать тот код, который кто-то написал бы на C вручную. Это определенно не относится к бэкэнду C на Haskell, что является одной из причин, почему бэкэнд C оптимизирован так плохо.

Еще одно замечание против использования бэкэнда C состоит в том, что семантика C на самом деле не так проста, как выглядит . Если ваш язык значительно отличается от C, использование бэкэнда C означает, что вам придется отслеживать все эти вызывающие раздражение сложности и, возможно, различия между компиляторами C. Может быть проще использовать LLVM с его более простой семантикой или разработать свой собственный бэкэнд, чем отслеживать все это.

3 голосов
/ 23 января 2012

Лично я бы скомпилировал на C. Таким образом, у вас есть универсальный язык-посредник, и вам не нужно беспокоиться о том, поддерживает ли ваш компилятор все платформы. Использование LLVM может привести к некоторому приросту производительности (хотя я бы сказал, что того же можно добиться, настроив генерацию кода на C так, чтобы он был более дружественным к оптимизатору), но это ограничит вас поддержкой только целей, поддерживаемых LLVM, и необходимостью ждать LLVM для добавления цели, когда вы хотите поддержать что-то новое, старое, другое или непонятное.

2 голосов
/ 13 декабря 2014

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

  1. Оптимизация вызовов в хвосте

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

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

  2. Сборка мусора

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

    Например, вместо использования обычных указателей C для объектов на исходном языке , можно создать специальную структуру, в которой может быть реализован алгоритм сборки мусора .Объекты на вашем языке (точнее, ссылки) - могут вести себя так же, как в Java, но в C они могут быть представлены вместе с метаинформацией (чего не было бы, если бы вы работали только с указателями).

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

  3. Отсутствие операций

    hippietrail отметил , что в C отсутствуют операторы поворота (под которыми я предполагаю, что он имел в виду круговое смещение), которые поддерживаются процессорами.Если такие операции доступны в наборе команд, то их можно добавить с помощью встроенной сборки .

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

Этот ответ , похоже, серьезно относится к некоторым основным проблемам.Хотелось бы увидеть еще одно обоснование того, какие именно проблемы вызваны семантикой Си.

2 голосов
/ 02 февраля 2012

Насколько я знаю, C не может запрашивать или манипулировать флагами процессора.

1 голос

Существует особый случай, когда вы пишете язык программирования со строгими требованиями безопасности * или надежности.

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

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

* обратите внимание, что «надежная безопасность» здесь совершенно не связана с тем, что банки и ИТ-компании утверждают, что имеют

...