Почему IL?

После анонса проекта Roslyn, одним из первых и наиболее популярных вопросов был следующий: «Roslyn – это что-то типа LLVM для .NET?».

Нет, Roslyn – это не LLVM для .NET. LLVM расшифровывается как Low-Level Virtual Machine; и, насколько я понимаю (честно говоря, никогда этой штукой не пользовался), работает это таким образом: компилятор принимает на вход код на некотором языке программирования, скажем, на С++, и преобразовывает его в эквивалентный код на языке LLVM. А затем другой компилятор принимает код на языке LLVM и преобразует его в оптимизированный машинный код.

Нечто подобное у нас уже есть для платформы .NET; по сути, .NET всегда был построен по такому принципу и Рослин здесь ни при чем. Компиляторы таких языков как C# или VB, принимают на вход программу, написанную на одном языке, и преобразовывают ее в Common Intermediate Language (CIL, который также часто называют MSIL или просто IL). Затем другой компилятор, либо jitter, который запускается прямо во время выполнения («just in time») или же NGEN, инструмент, запускаемый перед выполнением, преобразуют IL в оптимизированные машинные инструкции, которые уже могут выполняться на целевой платформе.

Меня иногда спрашивают о том, почему мы выбрали именно эту стратегию; почему компилятор C# не может напрямую генерировать машинный код без дополнительного промежуточного этапа? Зачем нам нужны два компилятора для преобразования кода на языке C# в машинные инструкции, когда достаточно было бы одного?

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

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

Предположим, у вас есть несколько языков программирования: C#, VB, F#, JScript .NET и т.д. И, предположим, у вас есть m сред времени выполнения: компьютеры с ОС Windows, работающие на процессорах x86 или x64, XBOX 360, телефоны, Silverlight, работающий на Мак-е… и предположим, что мы используем подход с одним компилятором. Какое количество генераторов кода вам придется написать внутри вашего компилятора? Для каждого языка программирования вам придется написать кодогенератор для каждого окружения, в результате, придется написать n x m генераторов.

Теперь, давайте предположим, что вместо этого, компилятор каждого языка программирования генерирует код на языке IL, и теперь, вам достаточно одного jitter-а для каждого окружения. И какое количество генераторов кода вам придется написать? Один для каждого языка, преобразующий исходный язык в IL, и один для каждого окружения, преобразующий из языка IL в инструкции для целевого окружения. Это всего n + m, что значительно меньше, чем n x m, для реальных значений n и m.

Более того, существуют и другие экономические показатели. Язык IL был специально разработан таким образом, чтобы упростить процесс преобразования исходного языка в IL. Я являюсь экспертом в семантическом анализе языка C#, но не в эффективной генерации кода для процессоров мобильных телефонов. Если бы мне пришлось писать новый генератор кода для каждой платформы, на которой работает .NET Framework, то я бы тратил все свое время не по назначению за написанием генераторов кода, вместо того, чтобы заниматься семантическими анализаторами.

Экономия средств проявляется и с другой стороны: если вам нужно добавить поддержку для нового процессора, то вам придется лишь разработать для него jitter и все языки, компилируемые в IL будут там работать; вам придется написать лишь *один* jitter, чтобы получить все n языков на вашей новой платформе.

Такой способ экономии средств, за счет использования промежуточного языка, вовсе не новый и восходит он к концу 60-х годов прошлого столетия. Моим любимым примером использования этой стратегии является Z-Machine от Infocom; разработчики из компании Infocom писали игры на языке (который назывался Zork Intermediate Language), который затем компилировался в промежуточный Z-Code. Затем они написали интерпретаторы этого языка под множество платформ. В результате чего, они могли писать n игр под m разных платформ, с трудозатратами n + m, а не n x m. (Такой подход давал еще и дополнительные преимущества: они могли реализовать механизм управления виртуальной памятью на железе, которое само не поддерживало виртуальную память; если игра не помещалась в доступную память, то интерпретатор мог выгрузить код, неиспользуемый в этот момент и загрузить его позднее.)

В следующий раз я расскажу о том, почему IL является именно таким языком, какой он есть.

Оригинал статьи