Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье описываются новые функции и улучшения производительности среды выполнения .NET для .NET 10. Оно обновлено для предварительной версии 5.
Девиртуализация метода интерфейса работы с массивами
Одной из областей фокуса для .NET 10 является сокращение затрат на абстракции популярных языковых функций. В стремлении к этой цели способность JIT девиртуализировать вызовы методов расширилась, чтобы охватывать методы интерфейсов массивов.
Рассмотрим типичный подход циклирования по массиву:
static int Sum(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
Эту структуру кода JIT легко оптимизирует, в основном потому, что нет виртуальных вызовов для анализа. Вместо этого JIT-код может сосредоточиться на удалении границ доступа к массиву и применении оптимизаций цикла, добавленных в .NET 9. В следующем примере добавляются некоторые виртуальные вызовы:
static int Sum(int[] array)
{
int sum = 0;
IEnumerable<int> temp = array;
foreach (var num in temp)
{
sum += num;
}
return sum;
}
Тип базовой коллекции понятен, и JIT-код должен быть в состоянии преобразовать этот фрагмент в первый. Однако интерфейсы массива реализуются по-разному от "обычных" интерфейсов, поэтому JIT не знает, как девиртуализировать их. Это означает, что вызовы перечислителя в цикле foreach
остаются виртуальными, блокируя несколько оптимизаций, таких как встраивание и выделение стека.
Начиная с .NET 10 JIT может девиртуализировать и встраивать методы интерфейса массива. Это первый из многих шагов для достижения паритета производительности между различными реализациями, как описано в планах де-абстракции .NET 10.
Отмена абстракции перечисления массива
Усилия по уменьшению абстрактных накладных расходов при итерации массивов с помощью перечислителей улучшили возможности JIT по встраиванию, выделению стека и клонированию циклов. Например, снижены затраты на перечисление массивов с помощью IEnumerable
, а условный анализ выхода теперь обеспечивает размещение перечислителей в стеке в определенных сценариях.
Улучшенный макет кода
Компилятор JIT в .NET 10 представляет новый подход к организации кода метода в базовые блоки для повышения производительности среды выполнения. Ранее JIT использовал обратный постпорядок (RPO) графа потока программы в качестве начального расположения, после которого следовали итеративные преобразования. Хотя этот метод был эффективен, у него были ограничения в моделировании компромиссов между уменьшением ветвления и увеличением плотности горячего кода.
В .NET 10 JIT моделирует проблему переупорядочения блоков как редукцию асимметричной задачи коммивояжёра и реализует 3-оптимический эвристический метод для нахождения практически оптимального обхода. Эта оптимизация повышает плотность горячих путей и уменьшает дистанции ветвлений, что приводит к повышению производительности времени выполнения.
Поддержка AVX10.2
.NET 10 предоставляет поддержку расширенных расширений векторов (AVX) 10.2 для процессоров на основе x64. Новые интринсики, доступные в классе System.Runtime.Intrinsics.X86.Avx10v2, можно проверить, когда будет доступно поддерживаемое оборудование.
Так как оборудование с поддержкой AVX10.2 еще недоступно, поддержка JIT для AVX10.2 в настоящее время отключена по умолчанию.
Выделение памяти в стеке
Выделение стека сокращает количество объектов, которые необходимо отслеживать, а также разблокирует другие оптимизации. Например, после выделения объекта на стеке, JIT может полностью заменить его скалярными значениями. Из-за этого выделение памяти в стеке является ключом к сокращению накладных расходов от абстракции ссылочных типов. .NET 10 добавляет выделение стека для небольших массивов типов значенийинебольших массивов ссылочных типов. Он также включает в себя анализ выходов для локальных полей структуры и делегатов. (Объекты, которые не могут быть освобождены, можно выделить на стеке.)
Небольшие массивы типов значений
JIT теперь выделяет в стеке небольшие массивы фиксированного размера для типов значений, которые не содержат указателей GC, когда можно гарантировать, что они не переживут родительский метод. В следующем примере JIT знает во время компиляции, что numbers
— это массив только из трех целых чисел, который не нужен после выполнения Sum
, и поэтому размещает его на стеке.
static void Sum()
{
int[] numbers = {1, 2, 3};
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
Console.WriteLine(sum);
}
Небольшие массивы ссылочных типов
.NET 10 расширяет улучшенные возможности выделения стека .NET 9 для небольших массивов ссылочных типов. Ранее массивы ссылочных типов всегда были выделены в куче, даже если их время существования было ограничено одним методом. Теперь JIT может выделить такие массивы стека, когда он определяет, что они не выходят за пределы контекста создания. В следующем примере массив words
теперь выделяется в стеке.
static void Print()
{
string[] words = {"Hello", "World!"};
foreach (var str in words)
{
Console.WriteLine(str);
}
}
Анализ escape-кода
Анализ escape-кода определяет, может ли объект перенять родительский метод. Объекты "убегают", если они назначены нелокальным переменным или передаются в функции, которые не встроены JIT-компилятором. Если объект не может выйти из локальной области до завершения функции, его можно разместить в стеке. .NET 10 включает анализ побега для:
Локальные поля структуры
Начиная с .NET 10, JIT рассматривает объекты, на которые ссылаются полями структур, что позволяет больше выделений в стеке и снижает перегрузку кучи. Рассмотрим следующий пример:
public class Program
{
struct GCStruct
{
public int[] arr;
}
public static void Main()
{
int[] x = new int[10];
GCStruct y = new GCStruct() { arr = x };
return y.arr[0];
}
}
Как правило, JIT осуществляет размещение на стеке небольших массивов фиксированного размера, которые не выходят за пределы, например x
. Его назначение y.arr
не приводит x
к ускользанию, так как y
также не ускользает. Однако предыдущая реализация анализа выхода JIT не моделировала ссылки на поля структур. В .NET 9 сборка x64, созданная для Main
, включает вызов CORINFO_HELP_NEWARR_1_VC
для выделения x
на куче, что указывает на то, что она была помечена как выходящая за пределы области видимости.
Program:Main():int (FullOpts):
push rax
mov rdi, 0x719E28028A98 ; int[]
mov esi, 10
call CORINFO_HELP_NEWARR_1_VC
mov eax, dword ptr [rax+0x10]
add rsp, 8
ret
В .NET 10 JIT больше не помечает объекты, на которые ссылаются локальные поля структуры, как выходящие за пределы текущей области видимости, если сама структура не выходит за эти пределы. Теперь сборка выглядит следующим образом (обратите внимание, что вызов вспомогательного средства выделения кучи исчез):
Program:Main():int (FullOpts):
sub rsp, 56
vxorps xmm8, xmm8, xmm8
vmovdqu ymmword ptr [rsp], ymm8
vmovdqa xmmword ptr [rsp+0x20], xmm8
xor eax, eax
mov qword ptr [rsp+0x30], rax
mov rax, 0x7F9FC16F8CC8 ; int[]
mov qword ptr [rsp], rax
lea rax, [rsp]
mov dword ptr [rax+0x08], 10
lea rax, [rsp]
mov eax, dword ptr [rax+0x10]
add rsp, 56
ret
Дополнительные сведения об улучшениях деабстракции в .NET 10 см. в dotnet/runtime#108913.
Делегаты
При компиляции исходного кода в IL каждый делегат преобразуется в класс закрытия с методом, соответствующим определению делегата и полям, соответствующим любым захваченным переменным. Во время выполнения создается объект замыкания для создания экземпляров захваченных переменных, а также объект Func
для вызова делегата. Если анализ выхода определяет, что объект не выйдет за пределы своей текущей области, JIT выделяет его на стеке.
Рассмотрим следующий Main
метод:
public static int Main()
{
int local = 1;
int[] arr = new int[100];
var func = (int x) => x + local;
int sum = 0;
foreach (int num in arr)
{
sum += func(num);
}
return sum;
}
Ранее JIT создает следующую сокращенную сборку x64 для Main
. Перед входом в цикл arr
, func
, и класс замыкания для func
, называемый Program+<>c__DisplayClass0_0
, все будут выделено в куче, как указано вызовами CORINFO_HELP_NEW*
.
; prolog omitted for brevity
mov rdi, 0x7DD0AE362E28 ; Program+<>c__DisplayClass0_0
call CORINFO_HELP_NEWSFAST
mov rbx, rax
mov dword ptr [rbx+0x08], 1
mov rdi, 0x7DD0AE268A98 ; int[]
mov esi, 100
call CORINFO_HELP_NEWARR_1_VC
mov r15, rax
mov rdi, 0x7DD0AE4A9C58 ; System.Func`2[int,int]
call CORINFO_HELP_NEWSFAST
mov r14, rax
lea rdi, bword ptr [r14+0x08]
mov rsi, rbx
call CORINFO_HELP_ASSIGN_REF
mov rsi, 0x7DD0AE461140 ; code for Program+<>c__DisplayClass0_0:<Main>b__0(int):int:this
mov qword ptr [r14+0x18], rsi
xor ebx, ebx
add r15, 16
mov r13d, 100
G_M24375_IG03: ;; offset=0x0075
mov esi, dword ptr [r15]
mov rdi, gword ptr [r14+0x08]
call [r14+0x18]System.Func`2[int,int]:Invoke(int):int:this
add ebx, eax
add r15, 4
dec r13d
jne SHORT G_M24375_IG03
; epilog omitted for brevity
Теперь, поскольку func
никогда не используется вне Main
области, он также выделяется на стеке:
; prolog omitted for brevity
mov rdi, 0x7B52F7837958 ; Program+<>c__DisplayClass0_0
call CORINFO_HELP_NEWSFAST
mov rbx, rax
mov dword ptr [rbx+0x08], 1
mov rsi, 0x7B52F7718CC8 ; int[]
mov qword ptr [rbp-0x1C0], rsi
lea rsi, [rbp-0x1C0]
mov dword ptr [rsi+0x08], 100
lea r15, [rbp-0x1C0]
xor r14d, r14d
add r15, 16
mov r13d, 100
G_M24375_IG03: ;; offset=0x0099
mov esi, dword ptr [r15]
mov rdi, rbx
mov rax, 0x7B52F7901638 ; address of definition for "func"
call rax
add r14d, eax
add r15, 4
dec r13d
jne SHORT G_M24375_IG03
; epilog omitted for brevity
Обратите внимание, что имеется один оставшийся CORINFO_HELP_NEW*
вызов, который представляет собой выделение памяти для замыкания. Команда среды выполнения планирует расширить анализ выхода для поддержки выделения замыканий на стеке в будущих выпусках.
Усовершенствования инлайнинга
В .NET 10 были внесены различные улучшения в области встраивания.
JIT теперь может встраивать методы, которые становятся подходящими для девиртуализации из-за предыдущего встраивания. Это улучшение позволяет JIT выявить больше возможностей для оптимизации, таких как дальнейшее встраивание и девиртуализация.
Некоторые методы, имеющие семантику обработки исключений, в частности блоки с try-finally
, также могут встраиваться.
Чтобы лучше воспользоваться способностью JIT выделять память для некоторых массивов на стеке, алгоритмы инлайнинга были скорректированы, чтобы увеличить эффективность кандидатов, которые могут возвращать небольшие массивы фиксированного размера.
Типы возвращаемых данных
Во время встраивания JIT теперь обновляет тип временных переменных, содержащих возвращаемые значения. Если все точки возврата в вызываемой функции дают один и тот же тип, эта конкретная информация о типе используется для девиртуализации последующих вызовов. Это усовершенствование дополняет улучшения в поздней девиртуализации и де-абстракции перечисления массивов.
Данные профиля
.NET 10 улучшает политику инлайнинга JIT, чтобы оптимально использовать данные профиля. Среди многочисленных эвристик встраиватель JIT не рассматривает методы, которые превышают определенный размер, чтобы избежать разрастания метода вызывающего объекта. Когда вызывающий объект имеет данные профиля, которые свидетельствуют о частом выполнении кандидата на встраивание, встраиватель увеличивает допустимый размер для данного кандидата.
Предположим, что JIT встроил некоторого вызываемого объекта Callee
без данных профиля в вызывающую сторону Caller
с данными профиля. Это несоответствие может произойти, если вызываемый слишком мал, чтобы его стоило инструментировать, или если он встраивается слишком часто, чтобы иметь достаточное количество вызовов. Если Callee
имеет собственных кандидатов на встраивание, JIT ранее не рассматривал их с учетом ограничения размера по умолчанию из-за отсутствия профильных данных в Callee
. Теперь JIT обработает данные профиля для Caller
, что позволит ослабить ограничение на его размер (однако, чтобы учитывать потерю точности, ослабление размера будет не таким значительным, как если бы у Callee
были данные профиля).
Аналогичным образом, когда JIT решает, что точка вызова не является прибыльной для встраивания, он помечает метод с NoInlining
для предотвращения рассмотрения в будущих попытках встраивания. Однако многие эвристики встраивания чувствительны к данным профилирования. Например, JIT может решить, что метод слишком велик, чтобы быть для встраивания, при отсутствии данных профиля. Но когда вызывающая функция достаточно интенсивно работает, JIT может быть готов ослабить ограничение на размер и встроить вызов. В .NET 10 JIT больше не помечает непригодные линии, чтобы избежать пессимизации сайтов вызовов NoInlining
с данными профиля.
Улучшения прединициализатора типа NativeAOT
Прединициализатор типа NativeAOT теперь поддерживает все варианты опкодов conv.*
и neg
. Это улучшение позволяет предварительную инициализацию методов, включающих операции приведения или отрицания, для дальнейшей оптимизации производительности времени выполнения.
Улучшения write-barrier для Arm64
Сборщик мусора .NET (GC) является поколенческим, что означает, что он отделяет живые объекты по возрасту, чтобы повысить производительность сборки мусора. СБ чаще собирает более молодые поколения, предполагая, что долгоживущие объекты с меньшей вероятностью останутся без ссылок (или "мертвыми") в любой момент времени. Однако предположим, что старый объект начинает ссылаться на молодой объект; GC должен знать, что он не может собрать молодой объект. Однако необходимость сканировать старые объекты, чтобы собрать молодой объект, сводит на нет увеличение производительности генерационного сборщика мусора.
Чтобы решить эту проблему, JIT вставляет барьеры записи перед обновлениями ссылок на объекты, чтобы обеспечить информирование GC. В x64 среда выполнения может динамически переключаться между реализациями барьера записи для балансировки скорости записи и эффективности сбора в зависимости от конфигурации GC. В .NET 10 эта функция также доступна в Arm64. В частности, новая базовая реализация барьера записи в Arm64 более точно управляет регионами GC, что повышает производительность сборки за счёт некоторого снижения пропускной способности записи. Тесты показывают улучшения приостановки GC с 8% до более чем 20% с новыми значениями по умолчанию GC.