Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 3개 있습니다.)
.NET Framework: 2086. C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법
; https://www.sysnet.pe.kr/2/0/13208

Windows: 218. 왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?
; https://www.sysnet.pe.kr/2/0/13209

Windows: 219. 윈도우 x64의 경우 0x00000000`7ffe0000 아래의 주소는 왜 사용하지 않을까요?
; https://www.sysnet.pe.kr/2/0/13210




왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?

아래와 글이 있군요. ^^

Why is address space allocation granularity 64KB?
; https://devblogs.microsoft.com/oldnewthing/20031008-00/?p=42223

지난 글에 정리한 대로, 가상 메모리를 할당하는 경우 일반 페이지 하나의 크기는 4KB인데요, 그렇게 할당은 해도 시스템은 64KB 선형 주소 공간을 한꺼번에 예약하는 구조입니다. 이 값들을 코드로는 다음과 같이 구할 수 있습니다.

using System.Runtime.InteropServices;

internal class Program
{
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern void GetSystemInfo(ref SYSTEM_INFO Info);

    static void Main(string[] args)
    {
        SYSTEM_INFO si = new SYSTEM_INFO();
        GetSystemInfo(ref si);

        Console.WriteLine($"allocation granularity: {FormatAddr(si.lpMinimumApplicationAddress)}");
        Console.WriteLine($"page granularity: 0x{si.dwPageSize:x8}");
    }

    private static string FormatAddr(nint address)
    {
        string text = $"{address:x16}";

        return $"0x{text.Substring(0, 8)}`{text.Substring(8)}";
    }
}

[StructLayout(LayoutKind.Sequential)]
internal struct SYSTEM_INFO
{
    internal ushort wProcessorArchitecture;
    internal ushort wReserved;
    internal uint dwPageSize;
    internal IntPtr lpMinimumApplicationAddress;
    internal IntPtr lpMaximumApplicationAddress;
    internal IntPtr dwActiveProcessorMask;
    internal uint dwNumberOfProcessors;
    internal uint dwProcessorType;
    internal uint dwAllocationGranularity;
    internal ushort wProcessorLevel;
    internal ushort wProcessorRevision;
}

실행해 보면,

allocation granularity: 0x00000000`00010000 // 0x10000 == 64KB
page granularity: 0x00001000 // 0x1000 == 4KB

2003년부터 지금까지 저 값은 64KB, 4KB입니다. (다만 CPU별로 달라질 수 있습니다.) 즉, 1바이트가 필요하다고 해도 4KB가 commit되며, 64KB의 선형 주소 공간이 예약되는 것입니다. 이에 대한 원인은 Alpha AXP RISC 프로세서의 동작 특성에서 기인했다고 합니다. RISC 프로세서의 경우 32bit 정숫값을 로드하는 명령어가 없었고, 대신 2개의 16비트 정수를 로드해 합치는 방식이었다고 합니다.

그래서 할당 단위를 64KB보다 작게 하려면 경우에 따라 16비트로 나뉜 2개의 값을 모두 바꿔야 하는 반면, 64KB 정렬이라고 가정하면 상위 16비트 정수 하나만 바꾸면 되기 때문에 성능상 이점이 있었던 것입니다.

아니, 그런데 왜? 64KB보다 작게 했을 때 2개의 값을 바꿔야만 했던 것일까요? 이게 잘 상상이 안 가는데요, 왜냐하면 Alpha AXP는 그것조차도 "부호 있는 정수" 2개를 32비트로 합치는 연산이었다고 합니다. 일례로, 0x1234abcd 주소를 지정하려면, 상위 0x1234는 상관없지만 하위 0xabcd는 음수이기 때문에 더하는 경우 0x1234abcd가 나올 수 없습니다. 따라서 이럴 때는 다음과 같이,

LDAH t1, 0x1235(zero) // t1 = 0x12350000
LDA  t1, -0x5433(t1) // t1 = t1 - 0x5433 = 0x1234abcd

차라리 상위를 0x1235로 지정하고 하위를 음수로 0x5433을 지정해 빼는 방식으로 처리해 0x1234abcd를 구합니다.

따라서 만약 가상 주소 공간의 정렬 단위가 64KB보다 작게 했다면, 이런 경우 DLL 로딩 시 발생하는 재배치 관련해서도 영향을 주게 됩니다. 즉, 모듈 재배치가 64KB의 하위 절반 값에서 64KB의 상위 절반 값으로 바뀐다면 signed 연산이라는 점을 감안해, 예를 들어 모듈의 기준값이 0x12345000이었다가, 재배치되는 주소가 0x12348000이 된다면 다시 LDAH/LDA 연산을 거쳐야만 주소를 지정할 수 있게 됩니다. 이런 방식은, 컴파일러의 명령어 재정렬 최적화까지 고려하면 문제가 더 복잡해진다고 합니다.

이 외에도 문제가 하나 더 있습니다. 예를 들어, 같은 64KB 영역에 있는 2개의 변수에 대한 주소를 계산할 필요가 있는 경우 컴파일러는 한 번의 LDAH 명령어를 공유하는 최적화를 수행할 수 있다고 합니다. 하지만 마찬가지로 64KB보다 작은 범위로 재배치를 허용한다면, 기존의 LDAH를 공유할 수 있었던 변수들조차도 재배치 이후에는 상위 값이 바뀔 수 있기 때문에 관련 최적화를 더 이상 수행할 수 없게 됩니다.

이런 모든 문제를 64KB 정렬로 강제하면 자연스럽게 해결이 됩니다.

자, 그럼 여기까지 완벽하게 이해하셨다면, 이제 여러분은 왜 2GB 메모리의 최상위 64KB 영역이 접근 불가능한 영역인지 알 수 있습니다.

예를 들어, (2GB의 최상위 64KB에 속한 주소의 한 예로) 0x7FFFABCD 주소를 가리키고 싶다고 가정해 보겠습니다. (이전에 예를 든 0x1234abcd처럼) 0x7fff, 0xabcd 주소를 signed 정수의 덧셈 연산으로 지정하려면 이렇게 해야만 합니다.

LDAH t1, 0x8000(zero) // t1 == 0x80000000
LDA  t1, -0x5433(t1)  // t1 = t1 - 0x5433 = 0x7fffabcd

여기서 문제는, LDAH의 상위에 지정한 0x8000 값이 더 이상 "signed 16 비트"로 지정 가능한 양수의 범위가 아니라는 점입니다. 한 가지 다행인 점은, 32비트 Windows NT였지만 재미있게도 Alpha AXP 프로세스는 64비트였다는 점입니다. 그래서 위의 연산은 64비트 Alpha AXP 프로세서에서 실제로는 다음과 같이 연산을 한 후,

LDAH t1, -0x8000(zero) // t1 == 0xFFFFFFFF`80000000
LDA  t1, -0x5433(t1)   // t1 = t1 - 0x5433 = 0xFFFFFFFF`7FFFFABCD

원래 우리가 원했던 0x7FFFFABCD를 얻기 위해 상위 0xFFFFFFFF를 지울 연산이 한 번 더 필요하게 되었고, 이를 위한 트릭으로 ADDL 명령을 이용할 수도 있었다고 합니다.

ADDL t1, zero, t1 // t1 = t1 + 0
// L 접미사가 붙은 ADD 연산은 operand를 32비트로 취급하고, 연산 결과를 64비트로 부호 확장합니다.
// 따라서 0xFFFFFFFF`7FFFFABCD 값은 32비트로 0x7FFFFABCD로 바뀌고,
// 0과 더한 연산 후의 결괏값을 64비트로 부호 확장해 0x00000000`7FFFABCD로 반환

그러니까, 결국 저 주소 영역을 허용해야 한다면 그 주소로의 재배치 가능성까지도 염두에 둬야 하고, 이로 인해 모든 메모리 주솟값 계산에 대해 ADDL 명령어까지 필요로 하게 됩니다.

Raymond Chen에 의하면, 사실 그 주소로의 재배치가 발생할 가능성이 희박한데도 불구하고 그런 경우까지도 고려해야 하는 명령어까지 넣어버리면 메모리 주소 계산 때마다 발생하는 50%의 성능 손실이 너무 컸다고 합니다. 결국 2GB의 마지막 64KB 영역은 "사용할 수 없는 영역(No man's land)"으로 취급하자는 결정에 이르게 된 것이라고.

물론, 현재 버전의 Windows는 Intel/AMD/ARM CPU만을 지원하므로 저런 제약이 없어 64KB 정렬을 유지하지 않아도 되었을 것입니다. 단지, 이미 안정화된 커널 코드인데다 64비트의 광활한 메모리 주소 공간으로 인해 64KB 정렬이라는 제약이 더 이상 제약이라고 할 수 없어 그대로 유지하는 것이 아닌가... 하는 개인적인 추측입니다.




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







[최초 등록일: ]
[최종 수정일: 11/20/2024]

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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  55  [56]  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12537정성태2/11/202119356.NET Framework: 1022. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기 [2]
12536정성태2/9/202118201개발 환경 구성: 542. BDP(Bandwidth-delay product)와 TCP Receive Window
12535정성태2/9/202117281개발 환경 구성: 541. Wireshark로 확인하는 LSO(Large Send Offload), RSC(Receive Segment Coalescing) 옵션
12534정성태2/8/202117797개발 환경 구성: 540. Wireshark + C/C++로 확인하는 TCP 연결에서의 closesocket 동작 [1]파일 다운로드1
12533정성태2/8/202116800개발 환경 구성: 539. Wireshark + C/C++로 확인하는 TCP 연결에서의 shutdown 동작파일 다운로드1
12532정성태2/6/202118002개발 환경 구성: 538. Wireshark + C#으로 확인하는 ReceiveBufferSize(SO_RCVBUF), SendBufferSize(SO_SNDBUF) [3]
12531정성태2/5/202116783개발 환경 구성: 537. Wireshark + C#으로 확인하는 PSH flag와 Nagle 알고리듬파일 다운로드1
12530정성태2/4/202120596개발 환경 구성: 536. Wireshark + C#으로 확인하는 TCP 통신의 Receive Window
12529정성태2/4/202118467개발 환경 구성: 535. Wireshark + C#으로 확인하는 TCP 통신의 MIN RTO [1]
12528정성태2/1/202118069개발 환경 구성: 534. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 윈도우 환경
12527정성태2/1/202118153개발 환경 구성: 533. Wireshark + C#으로 확인하는 TCP 통신의 MSS(Maximum Segment Size) - 리눅스 환경파일 다운로드1
12526정성태2/1/202114964개발 환경 구성: 532. Azure Devops의 파이프라인 빌드 시 snk 파일 다루는 방법 - Secure file
12525정성태2/1/202113931개발 환경 구성: 531. Azure Devops - 파이프라인 실행 시 빌드 이벤트를 생략하는 방법
12524정성태1/31/202115137개발 환경 구성: 530. 기존 github 프로젝트를 Azure Devops의 빌드 Pipeline에 연결하는 방법 [1]
12523정성태1/31/202116058개발 환경 구성: 529. 기존 github 프로젝트를 Azure Devops의 Board에 연결하는 방법
12522정성태1/31/202118253개발 환경 구성: 528. 오라클 클라우드의 리눅스 VM - 9000 MTU Jumbo Frame 테스트
12521정성태1/31/202117339개발 환경 구성: 527. 이더넷(Ethernet) 환경의 TCP 통신에서 MSS(Maximum Segment Size) 확인 [1]
12520정성태1/30/202116096개발 환경 구성: 526. 오라클 클라우드의 VM에 ping ICMP 여는 방법
12519정성태1/30/202114821개발 환경 구성: 525. 오라클 클라우드의 VM을 외부에서 접근하기 위해 포트 여는 방법
12518정성태1/30/202132921Linux: 37. Ubuntu에 Wireshark 설치 [2]
12517정성태1/30/202120631Linux: 36. 윈도우 클라이언트에서 X2Go를 이용한 원격 리눅스의 GUI 접속 - 우분투 20.04
12516정성태1/29/202117103Windows: 188. Windows - TCP default template 설정 방법
12515정성태1/28/202118751웹: 41. Microsoft Edge - localhost에 대해 http 접근 시 무조건 https로 바뀌는 문제 [3]
12514정성태1/28/202118900.NET Framework: 1021. C# - 일렉트론 닷넷(Electron.NET) 소개 [1]파일 다운로드1
12513정성태1/28/202116060오류 유형: 698. electronize - User Profile 디렉터리에 공백 문자가 있는 경우 빌드가 실패하는 문제 [1]
12512정성태1/28/202116446오류 유형: 697. The program can't start because VCRUNTIME140.dll is missing from your computer. Try reinstalling the program to fix this problem.
... 46  47  48  49  50  51  52  53  54  55  [56]  57  58  59  60  ...