Microsoft MVP성태의 닷넷 이야기
닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [링크 복사], [링크+제목 복사],
조회: 10889
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - .NET 7부터 추가된 Int128, UInt128

(gcc 등에서는 지원하지만) Visual C++도 지원하지 않는 Int128을 닷넷 8부터 추가해 C# 언어에서도 사용할 수 있게 되었습니다. ^^

Int128 Struct
; https://learn.microsoft.com/en-us/dotnet/api/system.int128

단지, 다른 타입과는 달리 C# 측에서 대응하는 alias가 없어 BigInteger를 사용하듯이 써야 합니다.

{
    Int128 value = long.MaxValue;
    value *= long.MaxValue;
    Console.WriteLine(value); // 85070591730234615847396907784232501249
}

{
    Int128 value = Int128.Parse("85070591730234615847396907784232501249");
    Console.WriteLine(value);
}

개인적으로 이걸 보고 궁금했던 게, Interlocked 측에서의 지원이 있느냐는 것이었습니다. 아쉽게도 여전히 (8바이트) long 형식까지만 지원하지만, 그래도 아예 불가능한 것은 아닙니다. 만들면 되니까요? ^^




이를 위해 우리는 inline asm 기법을 사용해야 합니다.

C++의 inline asm 사용을 .NET으로 포팅하는 방법
; https://www.sysnet.pe.kr/2/0/1267

게다가 InterlockedCompareExchange128 API를 구현한 소스 코드가 GitHub에 있으니,

Execute a InterlockedCompareExchange128 natively from C#
; https://gist.github.com/jduncanator/ab17e4e476300d3eb0b7c19f6f38429a

기왕이면 여기에 함수 포인터를 곁들여,

C# 9.0 - (6) 함수 포인터(Function pointers)
; https://www.sysnet.pe.kr/2/0/12374

다음과 같은 식으로,

using System.Runtime.InteropServices;

namespace int128sample;
public unsafe class InterlockedExtension
{   
    // Execute a InterlockedCompareExchange128 natively from C#
    // ; https://gist.github.com/jduncanator/ab17e4e476300d3eb0b7c19f6f38429a
    static byte[] asmCmpXchg16b = new byte[] {
                0x48, 0x89, 0x5C, 0x24, 0x08,  // MOV [RSP+0x8], RBX
                0x49, 0x8B, 0x01,              // MOV RAX, [R9]
                0x49, 0x89, 0xCA,              // MOV R10, RCX
                0x48, 0x89, 0xD1,              // MOV RCX, RDX
                0x4C, 0x89, 0xC3,              // MOV RBX, R8
                0x49, 0x8B, 0x51, 0x08,        // MOV RDX, [R9+0x8]
                0xF0, 0x49, 0x0F, 0xC7, 0x0A,  // LOCK CMPXCHG16B [R10]
                0x48, 0x8B, 0x5C, 0x24, 0x08,  // MOV RBX, [RSP+0x8]
                0x49, 0x89, 0x01,              // MOV [R9], RAX
                0x0F, 0x94, 0xC0,              // SETE AL
                0x49, 0x89, 0x51, 0x08,        // MOV [R9+0x8], RDX
                0xC2, 0x00, 0x00               // RET 0
            };

    public static delegate* unmanaged[Stdcall, SuppressGCTransition] _InterlockedCompareExchange128;
    static GCHandle _InterlockedCompareExchange128Handle;

    static InterlockedExtension()
    {
        _InterlockedCompareExchange128Handle = GCHandle.Alloc(asmCmpXchg16b, GCHandleType.Pinned);
        nint pData = (nint)_InterlockedCompareExchange128Handle.AddrOfPinnedObject().ToPointer();
        
        EnsureMemoryIsExecutable(pData, asmCmpXchg16b.Length);
        _InterlockedCompareExchange128 = (delegate* unmanaged[Stdcall, SuppressGCTransition])pData;
    }

    // ...[생략]...
}

기반을 만들 수 있습니다. 간단하게 테스트 코드는 이렇게 만들 수 있고!

{
    Int128 value = 0;
    Int128 comparand = 0;
    InterlockedExtension._InterlockedCompareExchange128((long*)&value, 0, 1, (long*)&comparand);
    Console.WriteLine(value); // 출력 결과: 1
}




그런데 실행해 보면, 지난 글에서 다룬 것과 동일한 aligned 문제가,

Visual C++ - InterlockedCompareExchange128 사용 방법
; https://www.sysnet.pe.kr/2/0/13472#align16

GC Heap 또는 스택에 할당된 Int128 변수에 적용되므로 이런 예외가 확률적으로 발생하게 됩니다.

Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at int128sample.Program.Main(System.String[])

만약 오류가 발생한다면 다음과 같이 변수 앞에 임시 조치를 취하면,

{
    long temporary = 0; // 8바이트 점유
    Int128 value = 0; // 이전에 8바이트 정렬이었다면 (운이 따르는 경우) temporary 변수로 인해 16바이트 위치로 변경
    // ...[생략]...
}

16바이트 정렬 효과를 갖게 돼 정상적으로 실행될 것입니다. 물론, 이 방법을 (release 빌드에서는 없어지는 문제도 있고, 최적화 시 재정렬될 수도 있으므로) 업무 코드에서 사용할 수는 없습니다. 그렇다면, C/C++의 경우 전역 변수를 사용하면 16바이트 정렬이 되었는데, C#은 어떨까요?

C#은 전역 변수라는 것이 없이, class 또는 struct 내에 static으로 흉내를 낼 수 있는데요,

internal unsafe class Program
{
    static Int128 g_value = 0;

    static void Main(string[] args)
    {
        fixed(Int128* ptr = &g_value)
        {
            Int128 value = 0;
            Int128 comparand = 0;
            InterlockedExtension._InterlockedCompareExchange128((long*)ptr, 0, 1, (long*)&comparand);
            Console.WriteLine(value);
        }
    }
}

이것 역시 확률적으로 crash가 발생합니다. 이유는, g_value가 GC Heap(HighFrequencyHeap)에 할당이 될 텐데 그런 경우 16바이트 정렬된 위치에 할당이 되리라는 것을 보장할 수 없기 때문입니다.

그렇다고 C#에 C/C++과 같은 "__declspec(align(16))"이 있는 것도 아니고... 난감하군요. ^^




자, 그렇다면 C#에서 해결해야 할 가장 큰 난제는 바로 정렬입니다. 이를 위해 StructLayout의 Pack이 있지만,

[StructLayout(LayoutKind.Sequential, Pack = 16)]
public struct Data
{
    byte __padding0; // (운에 따라) 이 위치가 0x0018이라면,
    public Int128 Value; // 이 위치는 16바이트를 건너 뛴 0x0028로 정렬
}

비록 alignment에 관여를 하긴 해도, 그것은 내부 멤버들의 정렬에 영향을 주는 것이기 때문에 애당초 저 구조체가 8바이트 정렬로 되는 것을 막을 수는 없습니다.

그렇다면 Win32 API를 interop 해야 할 것 같은데, 다행히 .NET 6부터 NativeMemory.AlignedAlloc이 제공되므로,

NativeMemory.AlignedAlloc(UIntPtr, UIntPtr) Method
; https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.nativememory.alignedalloc

이것을 감싸 다음과 같은 도우미 타입을 만들 수 있습니다.

public unsafe class NativeInt128 : IDisposable
{
    nint _value;

    public NativeInt128() : this(0)
    {
    }

    public NativeInt128(long value)
    {
        _value = (nint)NativeMemory.AlignedAlloc(16, 16);
        *(Int128*)_value = value;
    }

    public nint ValuePtr
    {
        get { return _value; }
    }

    public Int128 Value
    {
        get
        {
            return *(Int128*)_value;
        }
        set
        {
            *(Int128*)_value = value;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~NativeInt128()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_value == 0)
        {
            return;
        }

        NativeMemory.Free(_value.ToPointer());
        _value = 0;
    }
}

그런 다음, 이렇게 써야 그나마 안전하게 C#에서 Int128에 대한 InterlockedCompareExchange128 코드를 사용할 수 있습니다.

NativeInt128 data = new NativeInt128();
Int128 comparand = 0;
InterlockedExtension._InterlockedCompareExchange128((long*)data.ValuePtr, 0, 1, (long*)&comparand);

자, 여기까지 모두 준비되었으면 이제 InterlockedCompareExchange128을 이용해 Interlocked Increment 기능도 구현할 수 있습니다.

public void InterlockedIncrement()
{
    Int128 comparand = *(Int128*)_value;
    long* ptrLow;
    long* ptrHigh;

    Int128 newValue;
    ptrLow = (long*)&newValue;
    ptrHigh = ptrLow + 1;

    do
    {
        newValue = comparand + 1;
    } while (InterlockedExtension._InterlockedCompareExchange128((long*)_value,
                    *ptrHigh, *ptrLow, (long*)&comparand) == 0);
}

결국 이렇게 해서 구현하긴 했지만, 사실 이 과정을 그냥 하나의 응용 사례라고만 보시고 실제 코드는 단순히 lock을 쓰는 것이 훨씬 더 효율적입니다. ^^

object _lockValue = new object();

public void InterlockedIncrement()
{
    lock (_lockValue)
    {
        this.Value ++;
    }
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, System.Text.Json에서의 Int128 직렬화는 .NET 8부터 추가되었다고 합니다.

Built-in support for Half, Int128 and UInt128 numeric types
; https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-7/#built-in-support-for-half-int128-and-uint128-numeric-types




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/25/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-12-31 02시08분
NativeMemory.AlignedAlloc은 내부적으로 (윈도우의 경우) aligned_malloc을 호출합니다.

_aligned_malloc
; https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/aligned-malloc

참고로 Virtual 메모리 수준에서 메모리 정렬을 요구하는 함수도 있습니다.

How to allocate address space with a custom alignment or in a custom address region
; https://devblogs.microsoft.com/oldnewthing/20231229-00/?p=109204

VirtualAlloc2 function (memoryapi.h)
; https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc2
정성태

... 76  77  78  79  80  81  82  83  84  [85]  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11812정성태2/11/201915717오류 유형: 511. Windows Server 2003 VM 부팅 후 로그인 시점에 0xC0000005 BSOD 발생
11811정성태2/11/201921020오류 유형: 510. 서버 운영체제에 NVIDIA GeForce Experience 실행 시 wlanapi.dll 누락 문제
11810정성태2/11/201918576.NET Framework: 808. .NET Profiler - GAC 모듈에서 GAC 비-등록 모듈을 참조하는 경우의 문제
11809정성태2/11/201920779.NET Framework: 807. ClrMD를 이용해 메모리 덤프 파일로부터 특정 인스턴스를 참조하고 있는 소유자 확인
11808정성태2/8/201922111디버깅 기술: 123. windbg - 닷넷 응용 프로그램의 메모리 누수 분석
11807정성태1/29/201920002Windows: 156. 가상 디스크의 용량을 복구 파티션으로 인해 늘리지 못하는 경우 [4]
11806정성태1/29/201919646디버깅 기술: 122. windbg - 덤프 파일로부터 PID와 환경 변수 등의 정보를 구하는 방법
11805정성태1/28/201921820.NET Framework: 806. C# - int []와 object []의 차이로 이해하는 제네릭의 필요성 [4]파일 다운로드1
11804정성태1/24/201919664Windows: 155. diskpart - remove letter 이후 재부팅 시 다시 드라이브 문자가 할당되는 경우
11803정성태1/10/201918564디버깅 기술: 121. windbg - 닷넷 Finalizer 스레드가 멈춰있는 현상
11802정성태1/7/201920237.NET Framework: 805. 두 개의 윈도우를 각각 실행하는 방법(Windows Forms, WPF)파일 다운로드1
11801정성태1/1/201921536개발 환경 구성: 427. Netsh의 네트워크 모니터링 기능 [3]
11800정성태12/28/201820622오류 유형: 509. WCF 호출 오류 메시지 - System.ServiceModel.CommunicationException: Internal Server Error
11799정성태12/19/201822406.NET Framework: 804. WPF(또는 WinForm)에서 UWP UI 구성 요소 사용하는 방법 [3]파일 다운로드1
11798정성태12/19/201821246개발 환경 구성: 426. vcpkg - "Building vcpkg.exe failed. Please ensure you have installed Visual Studio with the Desktop C++ workload and the Windows SDK for Desktop C++"
11797정성태12/19/201817233개발 환경 구성: 425. vcpkg - CMake Error: Problem with archive_write_header(): Can't create '' 빌드 오류
11796정성태12/19/201817569개발 환경 구성: 424. vcpkg - "File does not have expected hash" 오류를 무시하는 방법
11795정성태12/19/201820840Windows: 154. PowerShell - Zone 별로 DNS 레코드 유형 정보 조회 [1]
11794정성태12/16/201816907오류 유형: 508. Get-AzureWebsite : Request to a downlevel service failed.
11793정성태12/16/201819457개발 환경 구성: 423. NuGet 패키지 제작 - Native와 Managed DLL을 분리하는 방법 [1]
11792정성태12/11/201819186Graphics: 34. .NET으로 구현하는 OpenGL (11) - Per-Pixel Lighting파일 다운로드1
11791정성태12/11/201819223VS.NET IDE: 130. C/C++ 프로젝트의 시작 프로그램으로 .NET Core EXE를 지정하는 경우 닷넷 디버깅이 안 되는 문제 [1]
11790정성태12/11/201817700오류 유형: 507. Could not save daemon configuration to C:\ProgramData\Docker\config\daemon.json: Access to the path 'C:\ProgramData\Docker\config' is denied.
11789정성태12/10/201831411Windows: 153. C# - USB 장치의 연결 및 해제 알림을 위한 WM_DEVICECHANGE 메시지 처리 [2]파일 다운로드2
11788정성태12/4/201817624오류 유형: 506. SqlClient - Value was either too large or too small for an Int32.Couldn't store <2151292191> in ... Column
11787정성태11/29/201821800Graphics: 33. .NET으로 구현하는 OpenGL (9), (10) - OBJ File Format, Loading 3D Models파일 다운로드1
... 76  77  78  79  80  81  82  83  84  [85]  86  87  88  89  90  ...