Search
Write a publication
Pull to refresh

Comments 17

Спасибо за статью.

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

Смотря что подразумевать под "нагруженный код". С одной стороны, C# всегда был богат инструментами для серьезных оптимизаций и выжать можно было много. С другой там где это по настоящему важно он будет сливать С++, Rust etc хоть с AOT хоть без него.

он будет сливать С++, Rust etc хоть с AOT хоть без него.

Вообще не факт. JIT-компилятор ориентируется на платформу, на которой запущен и может оптимизировать код под фичи процессора. А классическая картина для скомпилированных крестами бинарей - заточка под все процы, включая говно мамонта. И да, Native AOT работает медленнее IL кода с JIT. Но зато запускается моментально.

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

Старые истории про волшебный JIT который секретные оптимизации делает в рантайме и виртуалка начинает превосходит нативные языки. Что в Java что в C# одни и те же истории. Только почему то все забывают про серьезные накладные расходы самой виртуальной машины, которые для начала ей нужно как то компенсировать. И почему то никто не задумывается что для нативного языка тоже можно сделать PGO.

Вы разберитесь сначала, что за накладные расходы виртуальной машины такие, потом говорите. Для начала узнайте, что такое JIT-компилятор. И почему он называется "компилятор".

JIT который секретные оптимизации делает в рантайме

Если для вас векторные инструкции вроде AVX - это секретные оптимизации, то я вас слегка огорчу. Они ни для кого не секретные.

Вы разберитесь сначала, что за накладные расходы виртуальной машины такие, потом говорите. Для начала узнайте, что такое JIT-компилятор. И почему он называется "компилятор".

Неужели? Ну хорошо, вот мои тезисы человека несомненной непонимающего ничего.

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

Накладные расходы на поиски различных метаданных. К примеру когда метод вызывается в первый раз, его для начала нужно вообще найти, быть может его вообще не существует. После того как он найден, нужно проверить его корректность. Далее нужно провести JIT-компиляцию, скорректировать все метаданные и переходники т.д. Далее каждый вызов будет происходить через переходники а не напрямую. Не поверите, но все эти манипуляции далеко не бесплатные. И это все описание "прямого вызова". Вызов интерфейсного метода будет сопровождаться куда более увлекательной беготней по itables.

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

Ах да. Еще у нас кроссплатформенность, поэтому Вы не забиваете себе голову такими вопросами как: в какой кодировке находиться имя полученного файла. Удивительно, но в разных системах разные кодировки а работаем мы с ними одинаково, интересно почему? Может быть потому что для нашего удобства внутри приложения выделяется куча строк в едином формате чтобы нам было удобно.

Умеет виртуальная машина определять конкретное оборудование и проводить оптимизации под него, разумеется во время исполнения программы. Ну так, берете какой-нибудь Rust и делаете PGO и вуаля, мы тоже не лыком шиты.

Разумеется, все вышеперечисленное происходит бесплатно, потому что это волшебная магия.

Если для вас векторные инструкции вроде AVX - это секретные оптимизации, то я вас слегка огорчу. Они ни для кого не секретные.

Вот именно что ни для кого. Удивительно что по Вашей логике языки на VM в это чудо умеют а нативные - нет. А еще удивительней, что VM языки настолько в это умеют что в C# целый API сделан для ручной работы с векторными инструкциями и нигде не пишут мол - выбросьте из головы, чудо JIT сам решит все ваши проблемы. А в Java, учитывая что там есть векторизация на уровне JIT, почему то пилят ручной Vector API, интересно, зачем они это делают если должно произойти чудо и все само должно оптимизироваться?

Виртуальная машина

Нет в дотнете виртуальной машины.

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

Бред. Это компилируемый язык. Если метода нет, код не скомпилируется.

Далее каждый вызов будет происходить через переходники а не напрямую

Нет.

Если виртуальная машина хочет что-то оптимизировать, ей нужно собирать статистику использование.

Это вы сейчас с джавовым хотспотом путаете. Впрочем там тоже не так всё просто.

Еще у нас кроссплатформенность, поэтому Вы не забиваете себе голову такими вопросами как: в какой кодировке находиться имя полученного файла

Лол

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

Лол нет. Во время компиляции. Виртуальной машины не существует.

Разумеется, все вышеперечисленное происходит бесплатно, потому что это волшебная магия.

Всё вышеперечисленное просто не существует.

Удивительно что по Вашей логике языки на VM в это чудо умеют а нативные - нет.

Именно. Потому что для поддержки инструкций нужно компилить с поддержкой инструкций. Дотнет это может делать на целевой машине, а кресты - нет.

Нет в дотнете виртуальной машины.

https://learn.microsoft.com/en-us/dotnet/standard/clr

Бред. Это компилируемый язык. Если метода нет, код не скомпилируется.

https://learn.microsoft.com/en-us/dotnet/api/system.missingmethodexception?view=net-9.0

Лол нет. Во время компиляции. Виртуальной машины не существует.

https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/jit/ryujit-overview.md#execution-environment-and-external-interface

Всё вышеперечисленное просто не существует.

CLR - это не виртуальная машина. Это рантайм. Там нет никакой прослойки между исполняемым кодом и ОС.

MissingMethodException срабатывает только на typeof(App).InvokeMember(), что является частью рефлекшена.

Про ссылку на RyuJIT не понял ничего. Тоже компилер. Он не виртуальная машина. Интерфейс его - это рантайм компиляция IL кода из рефлекшена.

И картинка здесь зря. Не разбираетесь - не лезьте.

Вам в последней ссылке белым по черному написано:

Мне всё равно, что там написано. Clr не является виртуальной машиной в том смысле, в котором мы спорим. Там нет накладных расходов на работу кода. Это рантайм-библиотека вроде stdlib, только с GC, JIT и другими вещами. Этот вопрос ещё 2009 году обсуждался на SO

https://stackoverflow.com/questions/1564348/is-the-clr-a-virtual-machine

мы спорим

Мы не о чем не спорим, здесь нет места спору вообще.

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

Если CLR виртуальная машина для шарпа, то по этой логике - libc это виртуальная машина для си :D

Нет в дотнете виртуальной машины.

Лол нет. Во время компиляции. Виртуальной машины не существует.

Официальный сайт компании Microsoft.
Официальный .NET глоссарий.

https://learn.microsoft.com/ru-ru/dotnet/standard/glossary
https://learn.microsoft.com/en-us/dotnet/standard/glossary

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

Среда CLR обрабатывает выделение памяти и управление ею. Среда CLR также является виртуальной машиной, которая не только выполняет приложения, но и создает, а также компилирует код с помощью JIT-компилятора.

A CLR handles memory allocation and management. A CLR is also a virtual machine that not only executes apps but also generates and compiles code on-the-fly using a JIT compiler.

Если у Вас какие то свои термины отличимые от общепринятых, Ваше право. Хотите спорить дальше? Предлагаю написать официальное письмо в Microsoft где Вы можете изложить что они, в отличии от Вас, все глупые и не понимают что технология которую они создали, на самом деле не использует виртуальную машину. От дальнейшего принятия участия в спорах о терминах, которые Вы придумали сами для себя, воздержусь.

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

Потому что сейчас вы пытаетесь натянуть сову на глобус, цепляетесь к терминологии.

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

Давайте, покажите, какие издержки у этой "виртуальной машины". Что там "тормозит". По какой причине код шарпа в принципе не может быть быстрее c++/rust.

И очень забавно, что у @FFS_Studiosбыл только один коммент и только под вашей статьёй. Совпадение? И один плюс в карму при одном комменте. Вот же везунчик, а?

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

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

В какой формате находиться выполняемый код? В формате MSIL. Может ли операционная система запустить такое приложение? Нет не может и ничего про него не знает. Для того чтобы запустить приложение нам нужна программа которая будет эмулировать машину способную выполнять MSIL код, такие программы называются виртуальными машинами а точнее подвидом с названием process virtual machine. Можно даже на википедии найти такие базовые определения:

process virtual machine, sometimes called an application virtual machine, or Managed Runtime Environment (MRE), runs as a normal application inside a host OS and supports a single process. It is created when that process is started and deleted when it is closed. Its purpose is to provide a platform-independent programming environment that abstracts away details of the underlying hardware or operating system and allows a program to execute in the same way on any platform.

Кто у нас запускает приложение? Само? Нет. Мы запускаем виртуальную машину и уже она загружает приложение и готовит его к запуску. Как виртуальная машина исполняет приложение: интерпретирует(Python), либо интерпретирует с элементами JIT компиляции(JS/Java), либо полностью JIT компилирует. Это не важно, это средства реализации со своими плюсами и минусами.

Приложение запускается,происходит загрузка и JIT необходимых метаданных, методов и точки входа Main().

Пруф

Before an object instance is created, the CLR looks up the loaded types, loads the type if not found, obtains the MethodTable address, creates the object instance, and populates the object instance with the TypeHandle value. The JIT compiler-generated code uses TypeHandle to locate the MethodTable for method dispatching. The CLR uses TypeHandle whenever it has to backtrack to the loaded type through MethodTable.

Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects

Запускается Main(). Действительно теперь код работает почти как нативный. Дальше в ходе выполнения нужно вызывать метод другого класса который еще не загружен. Что делать? Правильно, Main() передаст управление виртуальной машине которая пойдет искать новый класс в метаданных сборки, затем они будет его верифицировать и загружать в память (мне никто не мешает пойти в IL код и руками сделать там все что угодно после того как оно скомпилирован, поэтому VM вынуждена проверять все в первый раз).

Пруф

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

Процесс управляемого выполнения

Далее, как только метаданные загружены в память, нужно искать сам метод в таблице методов, в зависимости от типа это будет более или менее сложно. Но зачем нам искать метод?

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

Пруф

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

Процесс управляемого выполнения

После того как мы нашли метод, нам нужно сделать JIT компиляцию. Это происходит во время выполнения только когда конкретный метод запускается первый раз. После того мы подменяем переходник в месте вызова и можем передать управление коду.

Скрытый текст

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

Процесс управляемого выполнения

Ах да, места вызовов. Допустим у нас есть класс A и метод fu() и он уже JITован. Далее мы вызываем метод bar() класса B. Если B не загружен мы вынуждены загрузить его. И вот проблема, у B весь код сырой в формате MSIL и ничего не знает про A. После загрузки B и JIT компиляции и вызова bar() он дойдет до fu(). Проблема в том что у bar() нет информации о том где лежит fu() и он вынужден ... искать этот метод в A и только потом менять переходник у себя. Выходит что мало просто один раз вызывать метод чтобы он JITанулся, нужно еще чтобы каждый целевой метод хотя бы раз его вызвал и поменял у себя переходники. Да, когда все они друг друга навызывают то код начнет работать почти как нативный. Почему почти? Потому что нативному коду это все не нужно, у него все посчитано при компиляции, ну максимум методы динамической библиотеки подключить да, у любого же языка с VM есть различные издержки в необходимости держать переходники и производить разрешения методов и метаданных. Такова их природа со своими плюсами и минусами.

Вот Вам мысли на подумать. И если требуете от меня что-то показывать и доказывать то извольте и сами.

Для того чтобы запустить приложение нам нужна программа которая будет эмулировать машину способную выполнять MSIL код

Я понял, что если кто-то в интернете не прав, то это не мои проблемы, а его.

Так что всего хорошего. Это вам с такими убеждениями жить дальше.

Давайте, покажите, какие издержки у этой "виртуальной машины". Что там "тормозит". По какой причине код шарпа в принципе не может быть быстрее c++/rust.

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

Почему для C# проблемно быть на уровне Rust/C++?

Если представим что в C# нет сборщика мусора то причина в природе его работы. Rust/С++ значительно проще, для них прямой вызов это прямой вызов, им не нужны переходника для методов, у них нет ленивой загрузки, валидации, нет динамики. CLR из-за своей природы вынуждена делать кучу доп вычислений: JIT, поиски и загрузки, подмены переходников, валидацию, оптимизации и прочее прямо во время выполнения программы. Rust/C++ делают все что им нужно во время компиляции и практически не ограничены во времени и поэтому могут применять оптимизации любой сложности. В тоже время VM язык ограничен во времени оптимизации очень сильно.

VM языки умеют во время выполнения использовать особенности конкретного процессора и т.д. прекрасно. Делаете PGO для Rust/C++ и получаете тоже самое. Что дальше то? Что еще могут предоставить VM языки, считающие все и вся по ходу выполнения?

Разумеется есть у них тузы в рукаве, но эти тузы не про производительность.

Sign up to leave a comment.

Articles