Подтвердить что ты не робот

Это хорошая идея для компиляции языка на C?

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

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

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

Ответ 7

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

Ответ 8

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

Ответ 9

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

  • Оптимизация запросов на хвост

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

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

  • Сбор мусора

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

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

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

  • Недействительные операции

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

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

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

Ответ 10

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

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

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

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