.NET Internals 10 Application execution model

.NET Internals 10 Application execution model


Дмитрий Бахтенков

Зная идею и основные преимущества JIT-компиляции из предыдущей статьи, мы теперь посмотрим, как она вписывается в модель выполнения .NET приложений.

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

Содержание:

  • Стандартизация .NET языков
  • Модель выполнения .NET-приложений

- Написание исходного кода на выбранном языке программирования

- Компиляция исходного кода в CIL

- Компиляция CIL в машинный код

- Обычная JIT-компиляция

- Предварительная JIT-компиляция

- Выполнение машинного кода

Стандартизация .NET языков

Как мы все знаем, .NET - это всего лишь платформа для выполнения. Однако существует множество языков программирования, которые позволяют нам писать код, который затем может быть выполнен с помощью специфичной для платформы (не специфичной для языка!) среды CLR. Чтобы предоставить создателям языков программирования набор правил, которым должен соответствовать их язык и его компилятор, чтобы быть совместимыми с .NET, Microsoft определила инфраструктуру общего языка (Common Language Infrastructure, CLI, также называемую MSIL в старые времена😉), которая также стандартизирована ISO и ECMA.

Основная цель этого состоит в том, чтобы определить, как исходный код должен быть скомпилирован на Общий промежуточный язык (CIL). Как мы уже знаем, CIL должен иметь стандартизированный формат, поскольку затем он JIT-компилируется в ассемблерный код для конкретной платформы. Некоторые из наиболее распространенных реализаций CLI - это .NET Framework, .NET Core и Mono. Я рекомендую вам также ознакомиться со статьей Мэтта Уоррена об истории среды выполнения .NET

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

Все это позволяет нам использовать различные языки программирования, такие как C#, VB.NET, F# или JScript .NET для разработки .NET-приложения. Любой человек все еще может реализовать свой собственный .Все это позволяет нам использовать различные языки программирования, такие как C#, VB.NET, F# или JScript .NET для разработки .NET-приложения. Любой человек все еще может реализовать свой собственный .NET-язык – он “просто” должен соответствовать стандартам.

Существует также еще один термин, введенный к .NET Core – .NET Standard. Его цель состоит в том, чтобы разрешить совместное использование кода между различными .СЕТЕВЫЕ реализации. На самом деле он определяет набор API, которые должна реализовывать каждая платформа .NET. Более подробная информация здесь.

Вся эта стандартизация заставляет нас прийти к модели выполнения .NET-приложений, которая стандартизирует выполнение любого приложение CLR.

Модель выполнения .NET-приложений

Независимо от выбранного языка модель выполнения приложения .NET можно описать как процесс, состоящий из 4 шагов:

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

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

.NET application execution model, source

Написание исходного кода на выбранном языке программирования

Как упоминалось ранее, приложения .NET могут быть реализованы с использованием любого языка по выбору, совместимого с CLI. Еще одной важной частью является компилятор – он определяет общий синтаксис языка, виды типов данных, которые могут использоваться программистом и т.д. Например, текущим компилятором с открытым исходным кодом для C# и Visual Basic является Roslyn.

В конце концов, роль компилятора заключается в преобразовании исходного кода в его эквивалент CIL.

Самое простое из возможных приложений, написанных на C#, может выглядеть следующим образом:  using System;

  public class Program
  {
      public static void Main()
      {
          Console.WriteLine("Hello Readers!");
      }
  }

Компиляция CIL в машинный код

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

Код CIL больше похож на ассемблерный код, однако содержит некоторые конкретные инструкции. Это все еще гораздо более читабельно, чем машинный код. Помимо прямой расшифровки исходного кода, CIL содержит метаданные о файле сборки DLL/EXE.

Фрагмент C#, представленный в предыдущем разделе, скомпилированный в CIL с помощью Roslyn, выглядит следующим образом:

  //  Microsoft (R) .NET Framework IL Disassembler.  Version 4.0.30319.33440
  //  Copyright (c) Microsoft Corporation.  All rights reserved.

  // Metadata version: v4.0.30319
  .assembly extern mscorlib
  {
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
  }
  .assembly '0bigfb2c'
  {
  .hash algorithm 0x00008004
  .ver 0:0:0:0
  }
  .module '0bigfb2c.dll'
  // MVID: {3DD8C852-9FE1-4825-8784-3D7200409F2F}
  .imagebase 0x10000000
  .file alignment 0x00000200
  .stackreserve 0x00100000
  .subsystem 0x0003       // WINDOWS_CUI
  .corflags 0x00000001    //  ILONLY
  // Image base: 0x01580000


  // =============== CLASS MEMBERS DECLARATION ===================

  .class public auto ansi beforefieldinit Program
  extends[mscorlib] System.Object
  {
  .method public hidebysig static void Main() cil managed
  {
  // 
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "Hello Readers!"
  IL_0006:  call       void [mscorlib]
  System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname
  instance void  .ctor() cil managed
  {
  // 
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call instance void [mscorlib]
  System.Object::.ctor()
  IL_0006:  ret
  } // end of method Program::.ctor

  } // end of class Program

Немного больше, чем исходный исходный код, не так ли?

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

Компиляция CIL в машинный код

Мы уже подробно описали этот шаг в предыдущей статье о JIT-компиляции, что является немного более причудливым названием этого процесса 🙂 Теперь вы можете увидеть, как и куда он вписывается модель выполнения .NET-приложения.

Обычная JIT-компиляция

По умолчанию JIT-компиляция выполняется в том месте, где отмечен "Normal JIT Compiler" на схеме ниже:

Normal JIT,

Инструкции IL конкретного блока кода, на диаграмме за пример взят метод Foo(), компилируются JIT, когда блок должен использоваться в первый раз. Затем этот JIT-скомпилированный код сохраняется как JIT-эквивалент кода метода Foo() в кэше памяти. Затем выполняется такой машинный код, и если в будущем потребуется снова вызвать тот же блок кода, он не будет повторно скомпилирован JIT, а вместо этого машинный код будет извлечен из кэша памяти и выполнен снова.

Существует возможность пропустить добавление собственного кода в кэш памяти для будущих исполнений, и он называется “Econo JIT Compilation”, однако он не широко используется и, вероятно, устарел с нескольких версий .NET, поэтому мы не будем его здесь рассматривать.

Предварительная JIT-компиляция

Схема, представленная выше, немного меняется, как только мы хотим использовать так называемую предварительную компиляцию JIT. Мы уже упоминали об этом, и мы знаем, что одним из методов, позволяющих выполнять компиляцию до JIT (или заранее), является использование собственного генератора нативных образов (Native Image Generator, ngen.exe), который позволяет преобразовывать сборки CIL в файлы собственного кода и хранить их в виде файла на диске (в кэше собственного образа). Это позволяет изначально компилировать целые сборки, однако это не позволяет компилировать только отдельные методы, как это делает JIT-компилятор.

Обратитесь к новой версии схемы, представленной ниже, чтобы увидеть, как меняется процесс с помощью NGen:

Pre-JIT

Обычный и предварительный режимы компиляции CIL имеют различные плюсы и минусы, в том числе:

  • NGen обеспечивает более быстрое время запуска, особенно в больших приложениях, используемых многими пользователями одновременно, но требует больше места на диске и памяти для хранения как CIL, так и предварительно скомпилированных образов
  • Компиляция JIT во время выполнения может обеспечить более быстрый код, поскольку он ориентирован на текущую платформу выполнения; NGen создает собственные образы с инструкциями, которые могут быть выполнены на всех возможных платформах, что означает, что он должен использовать самые старые из используемых в настоящее время наборов инструкций, чтобы быть обратно совместимым,
  • JIT способен динамически перекомпилировать код для повышения производительности в зависимости от условий выполнения.

Выбор одного из режимов зависит от вашего варианта использования

Выполнение машинного кода

Наконец, блоки кода запускаются средой CLR, которая запрашивает процессор для выполнения скомпилированных наборов инструкций. Если вас интересует, как именно работает процессор и выполняет инструкции, я рекомендую вам посмотреть это видео.

Report Page