Microsoft MVP성태의 닷넷 이야기
.NET Framework: 575. SharedDomain과 JIT 컴파일 [링크 복사], [링크+제목 복사],
조회: 19810
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 6개 있습니다.)
.NET Framework: 574. .NET - 눈으로 확인하는 SharedDomain의 동작 방식
; https://www.sysnet.pe.kr/2/0/10948

.NET Framework: 575. SharedDomain과 JIT 컴파일
; https://www.sysnet.pe.kr/2/0/10949

.NET Framework: 577. CLR Profiler로 살펴보는 SharedDomain의 모듈 로드 동작
; https://www.sysnet.pe.kr/2/0/10951

.NET Framework: 578. 도메인 중립적인 어셈블리가 비-도메인 중립적인 어셈블리를 참조하는 경우
; https://www.sysnet.pe.kr/2/0/10952

닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13568

닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC
; https://www.sysnet.pe.kr/2/0/13569




SharedDomain과 JIT 컴파일

지난 글에서, SharedDomain에 대해 알아봤는데요.

.NET - 눈으로 확인하는 SharedDomain의 동작 방식
; https://www.sysnet.pe.kr/2/0/10948

기왕 하는 김에 JIT 컴파일된 코드의 공유 여부도 확인해 보겠습니다.

SharedDomain에 올라왔다는 것은, 곧 JIT 컴파일된 기계어 코드도 공유된다는 의미입니다. 확인을 위해 지난번 예제에서 메서드의 JITted 주소를 출력하도록 수정해 보겠습니다.

우선 Main 메서드의 코드는 테스트 결과의 구분을 위해 LoaderOptimization.MultiDomainHost 모드로 선택하고,

// ============== ConsoleApplication1 프로젝트의 Main 메서드 ======================
using System;

namespace ConsoleApplication1
{
    partial class Program
    {
        [LoaderOptimization(LoaderOptimization.MultiDomainHost)]
        static void Main(string[] args)
        {
            new StrongDLL.Class1().DoMethod();
            new WeakDLL.Class1().DoMethod(); 

            Console.WriteLine();

            AppDomain appDomain = AppDomain.CreateDomain("TestAppDomain 1");
            appDomain.DoCallBack(
                () =>
                {
                    new StrongDLL.Class1().DoMethod();
                    new WeakDLL.Class1().DoMethod();
                }
            );

            Console.WriteLine("Press any key to exit...");
            Console.ReadLine();
        }
    }
}

GAC에 등록될 StrongDLL 프로젝트와, 등록되지 않을 WeakDLL 프로젝트에서는 해당 메서드 내부에서 JITted 주소를 출력하도록 변경했습니다.

// ============== StrongDLL, WeakDLL 라이브러리 프로젝트의 DoMethod 메서드 ======================
using System;
using System.Diagnostics;

namespace StrongDLL // WeakDLL 라이브러리의 경우 네임스페이스만 "WeakDLL"로 변경
{
    public class Class1
    {
        public void DoMethod()
        {
            Console.WriteLine(typeof(Class1).FullName + ".DoMethod called");

            StackFrame sf = new StackFrame();
            Console.WriteLine("JIT Address: 0x{0}", sf.GetMethod().MethodHandle.GetFunctionPointer().ToInt64().ToString("x"));
        }
    }
}

이렇게 변경하고 실행하면 다음과 같은 결과를 얻을 수 있습니다.

StrongDLL.Class1.DoMethod called
JIT Address: 0x7ffab7150c30
WeakDLL.Class1.DoMethod called
JIT Address: 0x7ffab7150e10

StrongDLL.Class1.DoMethod called
JIT Address: 0x7ffab7150c30
WeakDLL.Class1.DoMethod called
JIT Address: 0x7ffab71c0630

보는 바와 같이 LoaderOptimization.MultiDomainHost 모드에서는 GAC에 등록된 DLL이 SharedDomain에 로드되기 때문에 출력된 "StrongDLL.Class1.DoMethod"의 "JIT Address" 값도 동일합니다. 반면, 각각의 AppDomain에 로드된 WeakDLL.Class1.DoMethod 메서드의 "JIT Address"는 0x7ffab7150e10, 0x7ffab71c0630로 틀립니다.




참고로, windbg에서 확인하려면 다음과 같이 할 수 있습니다.

0:007> .loadby sos clr

0:007> !name2ee StrongDLL.dll!StrongDLL.Class1.DoMethod
Module:      00007ffab7034780
Assembly:    StrongDLL.dll
Token:       0000000006000001
MethodDesc:  00007ffab7034dc8
Name:        StrongDLL.Class1.DoMethod()
JITTED Code Address: 00007ffab7150c30

0:007> !name2ee WeakDLL.dll!WeakDLL.Class1.DoMethod
Module:      00007ffab7045cb8
Assembly:    WeakDLL.dll
Token:       0000000006000001
MethodDesc:  00007ffab7046350
Name:        WeakDLL.Class1.DoMethod()
JITTED Code Address: 00007ffab7150e10
--------------------------------------
Module:      00007ffab71a5dc0
Assembly:    WeakDLL.dll
Token:       0000000006000001
MethodDesc:  00007ffab71a6458
Name:        WeakDLL.Class1.DoMethod()
JITTED Code Address: 00007ffab71c0630

C# 코드에서 보여준 것과 결과는 같습니다. StrongDLL.Class1.DoMethod의 결과는 JIT 주소를 하나만 보여주고, WeakDLL.Class1.DoMethod는 2개의 AppDomain에서 각각 JIT 컴파일된 주소를 보여주고 있습니다.

출력된 JIT 주소가 어떤 AppDomain에 속해 있는지도 한번 알아볼까요? ^^ 예를 들기 위해 WeakDLL.Class1.DoMethod의 두 번째 출력인 00007ffab71c0630 주소에 대해 확인하는 방법은 다음과 같습니다.

0:007> !dumpmodule 00007ffab71a5dc0
Name:       C:\shared_domain\jitted_shareddomain_sample\ConsoleApplication1\bin\Debug\WeakDLL.dll
Attributes: PEFile 
Assembly:   00000241dfe0b690
LoaderHeap:              0000000000000000
TypeDefToMethodTableMap: 00007ffab71910a0
TypeRefToMethodTableMap: 00007ffab71910b8
MethodDefToDescMap:      00007ffab7191188
FieldDefToDescMap:       00007ffab71911a0
MemberRefToDescMap:      0000000000000000
FileReferencesMap:       00007ffab71911b0
AssemblyReferencesMap:   00007ffab71911b8
MetaData start address:  00000241dff220c0 (1852 bytes)

0:007> !dumpassembly 00000241dfe0b690
Parent Domain:      00000241dfd9abc0
Name:               C:\shared_domain\jitted_shareddomain_sample\ConsoleApplication1\bin\Debug\WeakDLL.dll
ClassLoader:        00000241dfe1abb0
  Module Name
00007ffab71a5dc0            C:\shared_domain\jitted_shareddomain_sample\ConsoleApplication1\bin\Debug\WeakDLL.dll

0:007> !dumpdomain 00000241dfd9abc0
--------------------------------------
Domain 2:           00000241dfd9abc0
LowFrequencyHeap:   00000241dfd9b3b8
HighFrequencyHeap:  00000241dfd9b448
StubHeap:           00000241dfd9b4d8
Stage:              OPEN
SecurityDescriptor: 00000241dfd9cc70
Name:               TestAppDomain 1
...[생략]...




이처럼, SharedDomain에 올라오는 어셈블리들은 JIT 컴파일된 기계어 코드를 공유하게 되어 있습니다. 그런데, 여기에서 재미있는 점이 하나 부각되는데, 바로 클래스의 '정적 데이터'에 대한 접근 문제가 그것입니다. 이게 왜 문제가 되냐면? 관리 환경에서 클래스의 정적 데이터는 (C/C++에서처럼 .exe 단위가 아니라) AppDomain 별로 격리되어 보관됩니다. 확인을 위해, 예제 코드의 Class1 타입을 다음과 같이 변경해 봅니다.

// ============== StrongDLL, WeakDLL 라이브러리 프로젝트 ======================
using System;
using System.Diagnostics;

namespace StrongDLL // WeakDLL 라이브러리의 경우 네임스페이스만 "WeakDLL"로 변경
{
    public class Class1
    {
        static int _count = 0;

        public void DoMethod()
        {
            Console.WriteLine(typeof(Class1).FullName + ".DoMethod called (times: " + (_count++) + ")");

            StackFrame sf = new StackFrame();
            Console.WriteLine("JIT Address: 0x{0}", sf.GetMethod().MethodHandle.GetFunctionPointer().ToInt64().ToString("x"));
        }
    }
}

실행하면 출력 결과는 다음과 같습니다.

StrongDLL.Class1.DoMethod called (times: 0)
JIT Address: 0x7ffab7140c30
WeakDLL.Class1.DoMethod called (times: 0)
JIT Address: 0x7ffab7140f90

StrongDLL.Class1.DoMethod called (times: 0)
JIT Address: 0x7ffab7140c30
WeakDLL.Class1.DoMethod called (times: 0)
JIT Address: 0x7ffab71b0680

왜냐하면 정적 데이터는 AppDomain 별로 격리되어 저장되기 때문인데요. 그런데, 이상하군요. StrongDLL.Class1.DoMethod의 경우 JIT 컴파일된 기계어 코드가 공유되는데 어떻게 해서 각자가 실행되는 AppDomain의 정적 데이터의 값을 정확하게 접근하고 있는 걸까요? 이에 대해서는 다음의 문서에 간략하게 소개되어 있습니다.

Application Domains
; https://learn.microsoft.com/en-us/dotnet/framework/app-domains/application-domains

Access to static data and methods is slower for domain-neutral assemblies because of the need to isolate assemblies. Each application domain that accesses the assembly must have a separate copy of the static data, to prevent references to objects in static fields from crossing domain boundaries. As a result, the runtime contains additional logic to direct a caller to the appropriate copy of the static data or method. This extra logic slows down the call.


그렇습니다. SharedDomain에 공유되는 domain-neutral assembly의 JIT 컴파일된 코드는 정적 데이터를 접근하는 경우 공유되지 않은 어셈블리의 동일한 코드보다 느립니다. 왜냐하면 개별 AppDomain의 문맥으로부터 정적 데이터를 가져오는 작업을 추가해야 하기 때문입니다.

따라서 이 글의 예제에 이 원칙을 적용하면, StrongDLL.dll은 SharedDomain에 로드되어 코드가 공유되기 때문에 StrongDLL.Class1.DoMethod의 수행이 WeakDLL.Class1.DoMethod의 것보다 (같은 코드를 가진 메서드임에도 불구하고) 성능상 손해를 보게 되는 것입니다. 어디... 확인을 해볼까요? ^^

우선, 별다른 오버헤드가 없을 WeakDLL.Class1.DoMethod의 JIT 컴파일된 코드를 windbg에서 확인해 보겠습니다. 메서드 정보를 찾아,

0:006> !name2ee WeakDLL.dll!WeakDLL.Class1.DoMethod 
Module:      00007ffab7035cb8
Assembly:    WeakDLL.dll
Token:       0000000006000001
MethodDesc:  00007ffab7036378
Name:        WeakDLL.Class1.DoMethod()
JITTED Code Address: 00007ffab7140f90
--------------------------------------
Module:      00007ffab7195dc0
Assembly:    WeakDLL.dll
Token:       0000000006000001
MethodDesc:  00007ffab7196480
Name:        WeakDLL.Class1.DoMethod()
JITTED Code Address: 00007ffab71b0680

출력된 각각의 주소에서 _count 정적 변수를 접근하는 부분을 찾아보면 다음과 같습니다.

0:006> !U 00007ffab7140f90
Normal JIT generated code
WeakDLL.Class1.DoMethod()
Begin 00007ffab7140f90, size 239
>>> 00007ffa`b7140f90 55              push    rbp
00007ffa`b7140f91 57              push    rdi
00007ffa`b7140f92 56              push    rsi
00007ffa`b7140f93 4881ecc0000000  sub     rsp,0C0h
...[생략]...
00007ffa`b714104a e88106645f      call    clr+0x16d0 (00007ffb`167816d0) (JitHelp: CORINFO_HELP_ARRADDR_ST)
00007ffa`b714104f 8b0def52efff    mov     ecx,dword ptr [00007ffa`b7036344]
00007ffa`b7141055 894dc8          mov     dword ptr [rbp-38h],ecx
00007ffa`b7141058 8b4dc8          mov     ecx,dword ptr [rbp-38h]
00007ffa`b714105b ffc1            inc     ecx
00007ffa`b714105d 890de152efff    mov     dword ptr [00007ffa`b7036344],ecx
00007ffa`b7141063 48b960af9804fb7f0000 mov rcx,offset mscorlib_ni+0x6baf60 (00007ffb`0498af60) (MT: System.Int32)
00007ffa`b714106d e8ce37645f      call    clr+0x4840 (00007ffb`16784840) (JitHelp: CORINFO_HELP_NEWSFAST)
...[생략]...
00007ffa`b71411c7 5d              pop     rbp
00007ffa`b71411c8 c3              ret


0:006> !U 00007ffab71b0680
Normal JIT generated code
WeakDLL.Class1.DoMethod()
Begin 00007ffab71b0680, size 239
>>> 00007ffa`b71b0680 55              push    rbp
00007ffa`b71b0681 57              push    rdi
00007ffa`b71b0682 56              push    rsi
00007ffa`b71b0683 4881ecc0000000  sub     rsp,0C0h
...[생략]...
00007ffa`b71b073a e8910f5d5f      call    clr+0x16d0 (00007ffb`167816d0) (JitHelp: CORINFO_HELP_ARRADDR_ST)
00007ffa`b71b073f 8b0d075dfeff    mov     ecx,dword ptr [00007ffa`b719644c]
00007ffa`b71b0745 894dc8          mov     dword ptr [rbp-38h],ecx
00007ffa`b71b0748 8b4dc8          mov     ecx,dword ptr [rbp-38h]
00007ffa`b71b074b ffc1            inc     ecx
00007ffa`b71b074d 890df95cfeff    mov     dword ptr [00007ffa`b719644c],ecx
00007ffa`b71b0753 48b960af9804fb7f0000 mov rcx,offset mscorlib_ni+0x6baf60 (00007ffb`0498af60) (MT: System.Int32)
00007ffa`b71b075d e8de405d5f      call    clr+0x4840 (00007ffb`16784840) (JitHelp: CORINFO_HELP_NEWSFAST)
...[생략]...
00007ffa`b71b08b7 5d              pop     rbp
00007ffa`b71b08b8 c3              ret

보는 바와 같이 JIT 컴파일된 코드의 크기는 같으면서 정적 변수의 주소값만 "[00007ffa`b7036344]", "[00007ffa`b719644c]"로 바뀌어 처리되고 있는 차이만 있습니다.

반면, 동일한 JIT 코드를 AppDomain간에 공유하는 StrongDLL.Class1.DoMethod 메서드의 경우,

0:006> !name2ee StrongDLL.dll!StrongDLL.Class1.DoMethod 
Module:      00007ffab7024780
Assembly:    StrongDLL.dll
Token:       0000000006000001
MethodDesc:  00007ffab7024df0
Name:        StrongDLL.Class1.DoMethod()
JITTED Code Address: 00007ffab7140c30

0:006> !U 00007ffab7140c30
Normal JIT generated code
StrongDLL.Class1.DoMethod()
Begin 00007ffab7140c30, size 251
>>> 00007ffa`b7140c30 55              push    rbp
00007ffa`b7140c31 57              push    rdi
00007ffa`b7140c32 56              push    rsi
00007ffa`b7140c33 4881ecc0000000  sub     rsp,0C0h
...[생략]...
00007ffa`b7140cea e8e109645f      call    clr+0x16d0 (00007ffb`167816d0) (JitHelp: CORINFO_HELP_ARRADDR_ST)
00007ffa`b7140cef b905000000      mov     ecx,5
00007ffa`b7140cf4 ba01000000      mov     edx,1
00007ffa`b7140cf9 e89241645f      call    clr+0x4e90 (00007ffb`16784e90) (JitHelp: CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE)
00007ffa`b7140cfe 8b4834          mov     ecx,dword ptr [rax+34h]
00007ffa`b7140d01 894dc8          mov     dword ptr [rbp-38h],ecx
00007ffa`b7140d04 b905000000      mov     ecx,5
00007ffa`b7140d09 ba01000000      mov     edx,1
00007ffa`b7140d0e e87d41645f      call    clr+0x4e90 (00007ffb`16784e90) (JitHelp: CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE)
00007ffa`b7140d13 8b4dc8          mov     ecx,dword ptr [rbp-38h]
00007ffa`b7140d16 ffc1            inc     ecx
00007ffa`b7140d18 894834          mov     dword ptr [rax+34h],ecx
00007ffa`b7140d1b 48b960af9804fb7f0000 mov rcx,offset mscorlib_ni+0x6baf60 (00007ffb`0498af60) (MT: System.Int32)
00007ffa`b7140d25 e8163b645f      call    clr+0x4840 (00007ffb`16784840) (JitHelp: CORINFO_HELP_NEWSFAST)
...[생략]...
00007ffa`b7140e7f 5d              pop     rbp
00007ffa`b7140e80 c3              ret

정적 변수를 그 문맥에 해당하는 AppDomain으로부터 가져와야 하기 때문에 (CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE)로 표시된 작업들을 사전에 해주고 있습니다. 성능상 느릴 수밖에 없습니다. (물론, 대부분의 경우 체감할 정도는 아니겠지만.)

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




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

[연관 글]






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

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

비밀번호

댓글 작성자
 




... 106  107  108  109  110  111  112  113  114  115  116  117  118  [119]  120  ...
NoWriterDateCnt.TitleFile(s)
10948정성태4/28/201623717.NET Framework: 574. .NET - 눈으로 확인하는 SharedDomain의 동작 방식 [3]파일 다운로드1
10947정성태4/27/201621580.NET Framework: 573. .NET CLR4 보안 모델 - 4. CLR4 보안 모델에서의 조건부 APTCA 역할파일 다운로드1
10946정성태4/26/201624449VS.NET IDE: 106. Visual Studio 2015 확장 - INI 파일을 위한 사용자 정의 포맷 기능 (Syntax Highlighting)파일 다운로드1
10945정성태4/26/201618174오류 유형: 327. VSIX 프로젝트 빌드 시 The "VsTemplatePaths" task could not be loaded from the assembly 오류 발생
10944정성태4/22/201619396디버깅 기술: 80. windbg - 풀 덤프 파일로부터 텍스트 파일의 내용을 찾는 방법
10943정성태4/22/201624247디버깅 기술: 79. windbg - 풀 덤프 파일로부터 .NET DLL을 추출/저장하는 방법 [1]
10942정성태4/19/201619606디버깅 기술: 78. windbg 사례 - .NET 예외가 발생한 시점의 오류 분석 [1]
10941정성태4/19/201619501오류 유형: 326. Error MSB8020 - The build tools for v120_xp (Platform Toolset = 'v120_xp') cannot be found.
10940정성태4/18/201622760Windows: 116. 프로세스 풀 덤프 시간을 줄여 주는 Process Reflection [3]
10939정성태4/18/201623798.NET Framework: 572. .NET APM 비동기 호출의 Begin...과 End... 조합 [3]파일 다운로드1
10938정성태4/13/201623359오류 유형: 325. 파일 삭제 시 오류 - Error 0x80070091: The directory is not empty.
10937정성태4/13/201631587Windows: 115. UEFI 모드로 윈도우 10 설치 가능한 USB 디스크 만드는 방법
10936정성태4/8/201642213Windows: 114. 삼성 센스 크로노스 7 노트북의 운영체제를 USB 디스크로 새로 설치하는 방법 [3]
10935정성태4/7/201626550웹: 32. Edge에서 Google Docs 문서 편집 시 한영 전환키가 동작 안하는 문제
10934정성태4/5/201625313디버깅 기술: 77. windbg의 콜스택 함수 인자를 쉽게 확인하는 방법 [1]
10933정성태4/5/201630897.NET Framework: 571. C# - 스레드 선호도(Thread Affinity) 지정하는 방법 [8]파일 다운로드1
10932정성태4/4/201623212VC++: 96. C/C++ 식 평가 - printf("%d %d %d\n", a, a++, a);
10931정성태3/31/201623479개발 환경 구성: 283. Hyper-V 내에 구성한 Active Directory 환경의 시간 구성 방법 [3]
10930정성태3/30/201621411.NET Framework: 570. .NET 4.5부터 추가된 CLR Profiler의 실행 시 Rejit 기능
10929정성태3/29/201631546.NET Framework: 569. ServicePointManager.DefaultConnectionLimit의 역할파일 다운로드1
10928정성태3/28/201637265.NET Framework: 568. ODP.NET의 완전한 닷넷 버전 Oracle ODP.NET, Managed Driver [2]파일 다운로드1
10927정성태3/25/201626506.NET Framework: 567. System.Net.ServicePointManager의 DefaultConnectionLimit 속성 설명
10926정성태3/24/201626013.NET Framework: 566. openssl의 PKCS#1 PEM 개인키 파일을 .NET RSACryptoServiceProvider에서 사용하는 방법 [10]파일 다운로드1
10925정성태3/24/201620340.NET Framework: 565. C# - Rabin-Miller 소수 생성 방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 - 두 번째 이야기파일 다운로드1
10924정성태3/22/201620934오류 유형: 324. Visual Studio에서 Azure 클라우드 서비스 생성 시 Failed to initialize the PowerShell host 에러 발생
10923정성태3/21/201622102.NET Framework: 564. C# - DGML로 바이너리 트리 출력하는 방법 [1]파일 다운로드1
... 106  107  108  109  110  111  112  113  114  115  116  117  118  [119]  120  ...