이 문서에서는 .NET 10용 .NET 런타임의 새로운 기능 및 성능 향상에 대해 설명합니다. 미리 보기 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
통해 배열을 열거하는 오버헤드가 줄어들고 조건부 이스케이프 분석을 통해 특정 시나리오에서 열거자를 스택 할당할 수 있습니다.
향상된 코드 레이아웃
.NET 10의 JIT 컴파일러는 런타임 성능을 향상하기 위해 메서드 코드를 기본 블록으로 구성하는 새로운 접근 방식을 도입했습니다. 이전에는 JIT에서 프로그램 흐름 그래프의 역방향 포스트오더 순회를 초기 레이아웃으로 사용한 후 반복 변환을 수행했습니다. 유효하지만 이 방법은 분기를 줄이고 핫 코드 밀도를 높이는 것 사이의 장차를 모델링하는 데 제한이 있었습니다.
.NET 10에서는 JIT가 블록 재배치 문제를 비대칭 여행 세일즈맨 문제의 축소로 모형화하고, 3-opt 휴리스틱을 구현하여 거의 최적에 가까운 순회를 찾아냅니다. 이 최적화는 핫 경로 밀도를 향상시키고 분기 거리를 줄여 런타임 성능을 향상시킵니다.
AVX10.2 지원
.NET 10에는 x64 기반 프로세서에 대한 AVX(Advanced Vector Extensions) 10.2 지원이 도입되었습니다. System.Runtime.Intrinsics.X86.Avx10v2 클래스에서 사용할 수 있는 새 내장 함수는 지원되는 하드웨어를 사용할 수 있게 되면 테스트할 수 있습니다.
AVX10.2 사용 하드웨어는 아직 사용할 수 없으므로 AVX10.2에 대한 JIT의 지원은 현재 기본적으로 사용하지 않도록 설정되어 있습니다.
스택 할당
스택 할당은 GC가 추적해야 하는 개체의 수를 줄이고 다른 최적화의 잠금을 해제합니다. 예를 들어 개체가 스택 할당된 후 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"되는 것으로, 지역 변수가 아닌 변수에 할당되거나 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
개체가 함께 생성됩니다. 이스케이프 분석에서 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
opcode의 모든 변형을 지원합니다. 이러한 향상된 기능을 통해 캐스팅 또는 부정 작업을 포함하는 메서드를 미리 초기화하여 런타임 성능을 더욱 최적화할 수 있습니다.
Arm64 쓰기 장벽 개선
. NET의 GC(가비지 수집기)는 세대별이므로 수집 성능을 향상시키기 위해 라이브 개체를 연령별로 구분합니다. GC는 수명이 긴 개체가 특정 시점에 참조가 끊기거나 "죽은" 상태가 될 가능성이 적다는 가정 하에 젊은 세대를 더 자주 수집합니다. 그러나 이전 개체가 젊은 개체를 참조하기 시작한다고 가정합니다. GC는 젊은 개체를 수집할 수 없다는 것을 알아야 합니다. 그러나 젊은 개체를 수집하기 위해 이전 개체를 스캔해야 하는 경우 세대 GC의 성능 향상이 저하됩니다.
이 문제를 해결하기 위해 JIT는 GC 정보를 유지하기 위해 개체 참조 업데이트 전에 쓰기 장벽을 삽입합니다. x64에서 런타임은 GC의 구성에 따라 쓰기 속도와 컬렉션 효율성의 균형을 맞추기 위해 쓰기 장벽 구현 간에 동적으로 전환할 수 있습니다. .NET 10에서 이 기능은 Arm64에서도 사용할 수 있습니다. 특히 Arm64의 새로운 기본 쓰기 장벽 구현은 GC 지역을 보다 정확하게 처리하므로 쓰기 장벽 처리량에 약간의 비용으로 컬렉션 성능이 향상됩니다. 벤치마크는 새 GC 기본값을 사용하여 GC 일시 중지가 8%에서 20% 이상으로 개선된 것을 보여줍니다.
.NET