Код MSIL и сравнение машинного кода (.NET)

Какие упрощения делаются при компиляции кода MSIL на какой-то конкретный компьютер? Ранее я думал, что машинный код не имеет операций на основе стека и что все операции на основе стека в MSIL преобразуются в многочисленные операции перемещения данных, которые имеют желаемый результат стека push / pop, и в результате машинный код обычно намного длиннее, чем MSIL. код. Но, похоже, это не так, поэтому я задаюсь вопросом - насколько машинный код отличается от кода MSIL и в каких аспектах?

Я был бы признателен за сравнение этих двух с разных точек зрения, например: Чем отличается количество операций / инструкций? В машинном коде обычно намного больше строк? Что еще, помимо независимости от платформы (по крайней мере, в смысле независимости от архитектуры процессора и от платформ на базе Windows), кода в стиле метаданных и того, что является своего рода "общим языком" для множества языков программирования высокого уровня, делает промежуточный / MSIL код разрешить? Какие могут быть наиболее заметные различия, если сравнить некоторый код MSIL и соответствующий машинный код?

Я бы очень признателен за сравнение в основном на высоком уровне, но, возможно, с некоторыми простыми и конкретными примерами.


person John P    schedule 28.02.2020    source источник
comment
Нет никакого значимого сравнения. Перевод может быть упрощенным, но это не то, что делает своевременный компилятор. Самая важная его работа - это оптимизация. MSIL был оптимизирован, чтобы быть максимально компактным, генерация машинного кода оптимизирована, чтобы быть максимально быстрой. Оптимизированный код в целом выглядит совсем иначе. Обзор используемых стратегий оптимизации находится здесь.   -  person Hans Passant    schedule 28.02.2020


Ответы (1)


Прежде всего, предположим, что «машинный код» означает x86-64 набор инструкций. С другими архитектурами, такими как ARM, отдельные аспекты могут немного отличаться.

Какие упрощения делаются при компиляции кода MSIL на какой-то конкретный компьютер?

На самом деле это не упрощения. MSIL и типичный набор машинных команд, такой как x86-64`, принципиально отличаются.

Ранее я думал, что машинный код не имеет операций на основе стека и что все операции на основе стека в MSIL преобразуются в многочисленные операции перемещения данных, которые имеют желаемый результат стека push / pop, и в результате машинный код обычно намного длиннее, чем MSIL. код.

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

Однако: основная концепция аппаратных процессоров - это регистры. Большинство вычислений и операций с памятью могут выполняться исключительно в регистрах, а не в основной памяти компьютера. Считайте их временными переменными. Кроме того, они работают намного быстрее, чем с основной памятью (даже несмотря на все уровни кешей между ними).

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

Но, похоже, это не так, поэтому я задаюсь вопросом - насколько машинный код отличается от кода MSIL и в каких аспектах?

У нас есть выражение C #: a = b + c * d;, где каждая переменная - это int.

В MSIL:

ldloc.1     // b — load from local variable slot 1
ldloc.2     // c — load from local variable slot 2
ldloc.3     // d — load from local variable slot 3
mul         // multiple two top-most values, storing the result on the stack
add         // add two top-most values, storing the result on the stack
stloc.0     // a — store top-most value to local variable slot 0

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

В x86-64 сборке:

mov   eax, dword ptr [c]   // load c into register eax
mul   dword ptr [d]        // multiply eax (default argument) with d
add   eax, dword ptr [b]   // add b to eax
mov   dword ptr [a], eax   // store eax to a

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

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

Чем отличается количество операций / инструкций?

Ответ: это зависит от обстоятельств. Некоторые простые операции, такие как арифметические операции, иногда 1: 1, например add в MSIL может привести к одному add в x86-64. С другой стороны, MSIL может использовать преимущество определения гораздо более высокоуровневых операций. Например, инструкция MSIL callvirt, которая вызывает виртуальный метод, не имеет простого аналога в x86-64: вам потребуется несколько инструкций для выполнения этого вызова.

В машинном коде обычно намного больше строк?

Мне нужны достоверные данные для сравнения; однако, учитывая вышесказанное относительно сложности инструкций, я бы сказал, скорее, да.

Что еще, кроме независимости от платформы и кода в стиле метаданных, допускает промежуточный код / ​​код MSIL?

Я думаю, что вопрос должен заключаться в следующем: что еще позволяет машинный код? MSIL довольно ограничен. CLR определяет множество правил, которые помогают поддерживать согласованность и правильность кода MSIL. В машинном коде у вас есть полная свобода - и вы тоже можете все испортить.

Какие могут быть наиболее заметные различия, если сравнить некоторый код MSIL и соответствующий машинный код?

С моей точки зрения, это основанная на регистрах архитектура процессоров, таких как x86-64.

Что упрощает MSIL, помимо этих функций? Какие естественные структуры / особенности языка MSIL упрощают некоторые вещи?

На самом деле их много. Во-первых, поскольку это архитектура на основе стека, гораздо проще скомпилировать язык программирования .NET в MSIL, как я объяснил ранее. Затем есть много других более мелких вещей, таких как:

  • MSIL естественно понимает все примитивные типы данных CLR (.NET).
  • MSIL может выражать преобразования типов
  • MSIL понимает объекты (экземпляры типов), может выделять экземпляры (newobj), вызывать методы, включая вызовы виртуальных методов (очень важно)
  • синтаксис для написания MSIL вручную поддерживает объектно-ориентированное структурирование кода, то есть поддержка MSIL, выражающая высокоуровневые концепции объектно-ориентированного программирования
  • MSIL обеспечивает поддержку упаковки / распаковки
  • MSIL поддерживает выброс и перехват исключений (это тоже большое дело)
  • В MSIL есть инструкции для синхронизации на основе мьютексов (блокировки)
person Ondrej Tucny    schedule 28.02.2020
comment
Под упрощениями я действительно имел в виду декомпозицию на множество строк машинного кода, но я вижу, что это неправильный взгляд на это. Это не просто разложение, это перевод. I think the question should rather be: what else does machine code allow? MSIL is rather restrictive. - Я думаю, что "разрешить" было неудачным выбором - я действительно должен был сказать, что MSIL упрощает помимо этих функций? Какие естественные структуры / особенности языка MSIL упрощают некоторые вещи? - person John P; 28.02.2020
comment
@JohnP Теперь я понимаю. Я добавил в свой ответ пару примеров. - person Ondrej Tucny; 28.02.2020