.NET8 최고의 성능 최적화 비GC 힙

머리말

.NET8의 JIT에는 Non-GC Heap이라는 새로운 메커니즘이 도입되었습니다. JIT는 이름처럼 GC에 의해 관리되지 않는 Non-GC Heap에 관련 개체가 할당되도록 보장할 수 있습니다. JIT는 이 객체가 GC에 의해 참조되지 않고 이 객체의 수명 주기 동안 루트 객체(GC에 의해 파괴되지 않는 객체)로 남아 있는지 확인해야 합니다. 원본 텍스트: .NET8 Ultimate Performance Optimization 비GC 힙

개요

이 메커니즘을 도입하는 이유는 무엇입니까? 먼저 코드를 살펴보겠습니다.

public static string GetPrefix() => "https://";
static void Main(string[] args)
{
  GetPrefix ();
}

여기서 GetPrefix 함수는 상수 문자열 값을 반환하며 해당 ASM은 다음과 같습니다.

mov  rax,185CAC02068h
mov  rax,qword ptr [rax]

두 개의 mov 명령어. 첫 번째는 개체 포인터에 대한 포인터이고 두 번째는 개체에 대한 포인터입니다. 두 가지 간단한 명령이지만 그 뒤에 있는 논리는 더 복잡합니다. 기본적으로
문자열 상수 값입니다. .NET7의 JIT는 또한 이 문자열 상수 값을 힙에 복사하여 문자열 개체에 할당합니다. 반환됨 개체에 대한 보조 포인터입니다. Heap 객체이기 때문에 GC에 의해 이동될 수 있고, 매번 새로운 주소를 얻어야 하므로 부담이 커지는 경우가 많습니다.

여기서 문제가 무엇입니까? 문자열 상수 값에 그렇게 많은 단계가 필요합니까? 오버헤드가 너무 크면 단순화할 수 있나요? 생각하기 쉬운 일반적인 방법이 있는데, 바로 이 문자열 상수값의 주소를 고정하는 것인데, 이 상수값을 사용해야 할 때마다 이 고정된 주소에서 직접 읽어올 수 있는데 괜찮을까요? GC 힙은 분명히 하드 코딩하여 수정할 수 없습니다.

물론 가능합니다.방법은 문자열 상수 값을 POH(Fixed Object Heap)에 넣어 GC가 이동하지 못하도록 하는 것입니다. 이는 GC 재활용 시 이동 비용을 절감하지만, 고정된 객체도 GC로 제어하기 때문에 문제를 근본적으로 해결하지는 못합니다.위 단계는 이동할 수 없다는 점만 제외하면 동일하며, POH는 루트 객체를 처리하지 않습니다. 재활용될 수 있으며 주소가 다른 데이터를 가리키며 오류가 발생합니다.

특징

이 문제를 완벽하게 해결하기 위해 이 글의 주인공인 Non-GC Heap이 등장한다. 여기에는 세 가지 특성이 있습니다.
1. JIT는 이 개체가 GC에서 참조되지 않도록 해야 합니다
. 2. 이 개체는 수명 주기 동안 항상 루트 개체였습니다
. 3. 언로드 가능한 컨텍스트의 일부가 될 수 없습니다.

GC 힙에는 작은 개체 힙(SOH - 85,000바이트보다 작은 개체), 큰 개체 힙(LOH - 85,000바이트보다 큰 개체), 고정 개체 힙(POH) 및 No-GC 힙이 분리되어 있다고 생각할 수 있습니다. GC
힙 FOH(동결 힙) 외부.

이제 JIT는 생성된 코드에서 개체에 액세스할 때 간접 참조를 방지하고 대신 개체의 주소를 직접 하드코딩합니다.

GetPrefix 함수의 ASM은 .NET8 Non-GC Heap에서 다음과 같습니다.

mov  rax,26180000218h
C3   ret

26180000218h는 객체 주소이고 mov가 직접 반환됩니다. mov 하나만 단순화한 것처럼 보이지만 실제로는 하드코딩된 고정 모드 주소를 통해 전체 문자열 상수 값, 즉 문자열 상수 값이 GC 힙 대신 FOH에 할당되는 원리를 단순화합니다. 말할 필요도 없이 성능이 크게 향상되었습니다. 다음은 13배의 성능 향상을 측정합니다.

Method Job Mean Ratio
GetPrefix .NET 7 1.3450 ns
GetPrefix .NET 8 0.0729 ns

기타 비GC 힙 작업

1: typeof(T)를 사용하여 생성된 RuntimeType 개체

public Type GetTestsType() => typeof(Tests);

2: Array.Empty()를 보다 효율적으로 만들기 위해 빈 배열이 Non-GC Heap에 할당됩니다.

public string[] Test() => Array.Empty<string>();

둘 다 .NET8의 다음 ASM과 유사하며 mov가 직접 반환됩니다.

mov rax,1A0814EAEA8
ret

3: 정적 값 유형 필드와 연결된 힙 개체에는 GC에서 참조하는 필드가 포함되어 있지 않습니다.

public partial class Tests
{
    private static readonly ConfigurationData s_config = ConfigurationData.ReadData();
    public TimeSpan GetRefreshInterval() => s_config.RefreshInterval;
    private struct ConfigurationData
    {
        public static ConfigurationData ReadData() => new ConfigurationData
        {
            Index = 0x12345,
            Id = Guid.NewGuid(),
            IsEnabled = true,
            RefreshInterval = TimeSpan.FromSeconds(100)
        };
        public int Index;
        public Guid Id;
        public bool IsEnabled;
        public TimeSpan RefreshInterval;
    }
}

RefreshInterval .NET7은 다음과 같습니다.

mov       rax,13D84001F78
mov       rax,[rax]
mov       rax,[rax+20]
ret

RefreshInterval .NET8은 다음과 같습니다.

mov       rax,20D9853AE48
mov       rax,[rax]
ret

4: 세대 간 GC 참조 판단

암호:

public class Tests
{
    public void Write()
    {
        string dst = "old";
        Write(ref dst, "new");
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void Write(ref string dst, string s) => dst = s;
}

쓰기는 .NET7 및 .NET8에서 다음과 같이 생성됩니다.

call      CORINFO_HELP_CHECKED_ASSIGN_REF
nop
ret

CORINFO_HELP_CHECKED_ASSIGN_REF는 "GC 쓰기 장벽"을 포함하는 JIT 도우미 함수입니다. 이 함수는 GC가 어떤 참조가 작성되고 있는지 추적할 수 있게 해주는 작은 코드 조각입니다. 예를 들어 할당이 진행 중이기 때문에 알아야 할 수도 있습니다. made 객체는 gen0일 수 있고 대상은 gen2일 수 있습니다.

이 코드를 미세 조정하세요.

public class Tests
{
    public void Write()
    {
        string dst = "old";
        Write(ref dst);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void Write(ref string dst) => dst = "new";
}
  • 구현된 함수는 dst가 상수 문자열을 직접 할당한다는 점만 제외하면 동일합니다. 상수 문자열은 Non-GC Heap에 할당된다는 점을 기억하시나요? .NET7에는 도우미 기능이 여전히 필요합니다.
mov       rdx,1FF0E4014A0
mov       rdx,[rdx]
call      CORINFO_HELP_CHECKED_ASSIGN_REF
nop
ret

그러나 .NET8에서는

mov       rax,1B3814EAEC8
mov       [rcx],rax
ret

.NET8은 비GC 힙에 상수 문자열이 있다는 것을 인식하므로, card_table과 마찬가지로 그 안에 어떤 코드가 있는지 확인하기 위해 GC 추적이 필요하지 않습니다. 따라서 CORINFO_HELP_CHECKED_ASSIGN_REF가 최적화되었습니다.

 

과거의 하이라이트:

.NET8 JIT 코어: 계층화된 컴파일의 원리

.Net 새 버전의 성능이 C++의 90%에 도달합니까?

면접관은 .Net 개체에 null 값이 할당되면 GC에서 재활용됩니까?

.Net JIT의 멋진 운영에 대한 DNGuard HVM 원리에 대한 간략한 분석

 

저자: 강호 논평. 공개 계정: jianghupt 팔로우를 환영합니다. 기사가 처음 나온 곳.

OpenAI는 모든 사용자에게 ChatGPT Voice Vite 5를 무료로 공개합니다. 공식 출시됩니다. 운영자의 마법 작전: 백그라운드에서 네트워크 연결을 끊고, 광대역 계정을 비활성화하고, 사용자가 광 모뎀을 강제로 변경하도록 합니다. Microsoft 오픈 소스 터미널 채팅 프로그래머가 ETC 잔액을 조작하고 연간 260만 위안 이상 횡령 Redis의 아버지가 사용하는 Pure C 언어 코드는 Telegram Bot 프레임워크를 구현합니다. 오픈 소스 프로젝트 관리자라면 이런 답변을 어디까지 견딜 수 있습니까? Microsoft Copilot Web AI는 중국 OpenAI를 지원하는 12월 1일 공식 출시될 예정입니다 . 전 CEO이자 사장인 Sam Altman과 Greg Brockman이 Microsoft에 합류했습니다. Broadcom은 VMware의 성공적인 인수를 발표했습니다.
{{o.이름}}
{{이름}}

おすすめ

転載: my.oschina.net/u/5407571/blog/10150775
おすすめ