성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>.NET Core 2.1 - Tiered Compilation 도입</h1> <p> 아래의 글에 tiered compilation에 대한 소개가 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Announcing .NET Core 2.1 ; <a target='tab' href='https://devblogs.microsoft.com/dotnet/announcing-net-core-2-1/'>https://devblogs.microsoft.com/dotnet/announcing-net-core-2-1/</a> </pre> <br /> "Adaptive optimization"이라고도 알려진 이것의 개념은 간단합니다. 이 옵션이 켜져 있으면 해당 응용 프로그램은 JIT 컴파일 시에 최적화보다는 빠른 속도를 위주로 기계어 번역을 하게 됩니다. 이 단계를 "first tier"라고 합니다. 그러다, 자주 실행되는 메서드가 있으면 그에 대해 감지하고 다시 최적화된 코드로 JIT 컴파일을 하는 식인데 이를 "second tier"라고 합니다.<br /> <br /> 과거 자바 런타임이 했던 방식과 유사한 면이 있습니다. 자바의 경우 최초 실행 시에 JIT하지 않고 바이트 언어를 그냥 인터프리팅 식으로 그때그때 해석해서 실행하는데, 자주 실행되는 함수라고 판단이 되면 그제서야 JIT를 해 기계어로 번역하는 식입니다.<br /> <br /> "tiered compilation" 적용 방법은 프로젝트 파일에 TieredCompilation 옵션을 true로 설정하거나,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.1</TargetFramework> <Description>A simple .NET Core global tool called "dotnetsay".</Description> ...[생략]... <span style='color: blue; font-weight: bold'><TieredCompilation>true</TieredCompilation></span> </PropertyGroup> <ItemGroup Condition="'$(ContinuousIntegrationBuild)'=='true'"> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-62925-02" PrivateAssets="All"/> </ItemGroup> </Project> </pre> <br /> .NET Core 2.1 이상의 응용 프로그램을 실행 시 환경 변수에 COMPlus_TieredCompilation을 "1"로 설정하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > SET COMPlus_TieredCompilation="1" </pre> <br /> (2023-11-20 업데이트: .NET Core 3.0+부터는 TieredCompilation 옵션의 기본값이 enabled로 바뀌었습니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 혹시 눈으로 직접 확인해 볼 수 있을까요? ^^<br /> <br /> 그래서 예제 코드를 하나 준비해봤습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Reflection; using System.Threading; namespace ConsoleApp1 { <span style='color: blue; font-weight: bold'>// 빌드: .NET Core x64 + Release</span> class Program { static void Main(string[] args) { Thread t = new Thread(CheckMethodJitAddress); t.IsBackground = true; Program pg = new Program(); { <span style='color: blue; font-weight: bold'>pg.ManyCalls();</span> CheckMethodJitAddress(false); } <span style='color: blue; font-weight: bold'>Console.ReadLine(); // first-tier 확인을 위해!</span> t.Start(true); Thread.Sleep(1000); while (true) { <span style='color: blue; font-weight: bold'>pg.ManyCalls(); // 아마 이 루프의 어디선가 second-tier로 진행할 듯!</span> Thread.Sleep(1000); } } public long ManyCalls() { long sum = 0; for (int i = 0; i < (Environment.TickCount % 10_000); i ++) { sum += i; } return sum; } private static void CheckMethodJitAddress(object obj) { do { MethodInfo mi = typeof(Program).GetMethod("ManyCalls"); RuntimeMethodHandle rmh = mi.MethodHandle; Console.WriteLine(rmh.GetFunctionPointer().ToString("x")); Thread.Sleep(1000); } while ((bool)obj == true); } } } </pre> <br /> 위의 코드를 실행하면, ManyCalls라는 메서드를 1초에 한 번씩 계속 실행하는데 다른 스레드에서는 그것의 FunctionPointer를 구해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 상황별 GetFunctionPointer 반환값 정리 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1027'>http://www.sysnet.pe.kr/2/0/1027</a> </pre> <br /> 출력합니다. JIT가 2번째가 되면 FunctionPointer의 주소가 바뀔 거라고 생각한 건데요, 의외로 계속 바뀌지 않고 그냥 고정된 값을 출력합니다. 음... ^^ 딴 방법을 이용해야 할 것 같습니다.<br /> <br /> 이를 위해 windbg를 이용해 봤는데요, sos 확장을 로드하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:009> <span style='color: blue; font-weight: bold'>.loadby sos coreclr</span> </pre> <br /> ManyCalls의 JIT 이후의 기계어 코드 위치를 다음과 같이 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:009> <span style='color: blue; font-weight: bold'>!name2ee ConsoleApp1!ConsoleApp1.Program</span> Module: 00007ff7b2024578 Assembly: ConsoleApp1.dll Token: 0000000002000002 MethodTable: <span style='color: blue; font-weight: bold'>00007ff7b2025560</span> EEClass: 00007ff7b21c1088 Name: ConsoleApp1.Program 0:009> <span style='color: blue; font-weight: bold'>!dumpmt -md 00007ff7b2025560</span> EEClass: 00007ff7b21c1088 Module: 00007ff7b2024578 Name: ConsoleApp1.Program mdToken: 0000000002000002 File: E:\ConsoleApp1\bin\Release\netcoreapp2.1\ConsoleApp1.dll BaseSize: 0x18 ComponentSize: 0x0 Slots in VTable: 8 Number of IFaces in IFaceMap: 0 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 00007ff807aa2020 00007ff807600988 PreJIT System.Object.ToString() 00007ff807aa2040 00007ff807600990 PreJIT System.Object.Equals(System.Object) 00007ff807aa2090 00007ff8076009b8 PreJIT System.Object.GetHashCode() 00007ff807aa20a0 00007ff8076009d8 PreJIT System.Object.Finalize() 00007ff7b21410b0 00007ff7b2025550 JIT ConsoleApp1.Program..ctor() 00007ff7b2141098 00007ff7b2025508 JIT ConsoleApp1.Program.Main(System.String[]) <span style='color: blue; font-weight: bold'>00007ff7b21410a0 00007ff7b2025520 JIT ConsoleApp1.Program.ManyCalls()</span> 00007ff7b21410a8 00007ff7b2025538 JIT ConsoleApp1.Program.CheckMethodJitAddress(System.Object) </pre> <br /> 00007ff7b21410a0 값은 실제로 GetFunctionPointer가 반환한 값과 일치합니다. ManyCalls 메서드에 대해 좀 더 살펴보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:009> <span style='color: blue; font-weight: bold'>!DumpMD /d 00007ff7b2025520</span> Method Name: ConsoleApp1.Program.ManyCalls() Class: 00007ff7b21c1088 MethodTable: 00007ff7b2025560 mdToken: 0000000006000002 Module: 00007ff7b2024578 IsJitted: yes <span style='color: blue; font-weight: bold'>Current CodeAddr: 00007ff7b2142010</span> Code Version History: <span style='color: blue; font-weight: bold'>CodeAddr: 00007ff7b2142010 (Tier 0)</span> NativeCodeVersion: 0000000000000000 </pre> <br /> 최초 한 번 실행된 상태이기 때문에 "Tier 0" 단계임을 알 수 있습니다. 그리고 응용 프로그램을 계속 실행해 ManyCalls를 어느 정도 실행한 시점에 다시 windbg로 멈추고 덤프를 해보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:010> <span style='color: blue; font-weight: bold'>!DumpMD /d 00007ff7b2025520</span> Method Name: ConsoleApp1.Program.ManyCalls() Class: 00007ff7b21c1088 MethodTable: 00007ff7b2025560 mdToken: 0000000006000002 Module: 00007ff7b2024578 IsJitted: yes <span style='color: blue; font-weight: bold'>Current CodeAddr: 00007ff7b21439e0</span> Code Version History: <span style='color: blue; font-weight: bold'>CodeAddr: 00007ff7b21439e0 (Tier 1)</span> NativeCodeVersion: 000001e92a9693c0 CodeAddr: 00007ff7b2142010 (Tier 0) NativeCodeVersion: 0000000000000000 </pre> <br /> 보는 바와 같이 JIT CodeAddr 위치가 바뀌었고 Tier 1이라고 보여줍니다. 즉, 실제로 JIT가 대상 메서드에 대해 2번 발생한 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데 GetFunctionPointer 반환값은 무엇일까요? Tier 1 단계에서 해당 위치를 역어셈블 해보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:010> <span style='color: blue; font-weight: bold'>u 00007ff7b21410a0</span> 00007ff7`b21410a0 e93b290000 <span style='color: blue; font-weight: bold'>jmp 00007ff7`b21439e0</span> ...[생략]... </pre> <br /> 보는 바와 같이 jmp 문의 위치가 바로 GetFunctionPointer의 값입니다. jmp 문의 기계어를 보면 e93b290000인데, 5바이트 중 첫 번째 e9이 jmp이고 이후의 4바이트가 점프할 상대 변위(offset) 값입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > jmp == e9 offset == 3b290000 </pre> <br /> 실제로 GetFunctionPointer가 반환한 00007ff7b21410a0 주소와 "!dumpmd"로 확인한 "Current CodeAddr"의 00007ff7b21439e0 주소의 차이를 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:010> <span style='color: blue; font-weight: bold'>? 00007ff7`b21439e0 - 00007ff7`b21410a0</span> Evaluate expression: 10560 = 00000000`00002940 0:010> <span style='color: blue; font-weight: bold'>? 00007ff7`b21439e0 - 00007ff7`b21410a0 - 5</span> Evaluate expression: 10555 = <span style='color: blue; font-weight: bold'>00000000`0000293b</span> </pre> <br /> 0000293b 값이 나옵니다. little endian 저장임을 감안하면 e93b290000 기계어 코드와 정확히 일치합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> windbg를 통해 알게 된 지식으로 이제 C#에서의 확인 코드를 다음과 같이 작성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private static void CheckMethodJitAddress(object obj) { do { MethodInfo mi = typeof(Program).GetMethod("ManyCalls"); RuntimeMethodHandle rmh = mi.MethodHandle; IntPtr ptr = rmh.GetFunctionPointer(); <span style='color: blue; font-weight: bold'>byte jmpCode = Marshal.ReadByte(ptr); // jmp 문 int offset = Marshal.ReadInt32(ptr + 1); // offset 값</span> Console.WriteLine(jmpCode.ToString("x") + ": " + offset.ToString("x8")); Thread.Sleep(1000); } while ((bool)obj == true); } </pre> <br /> 이렇게 바꾸고 실행하면 다음과 같은 출력 결과를 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > called: 1 <span style='color: blue; font-weight: bold'>e9: 0000108b</span> <span style='color: blue; font-weight: bold'>e8: 0000041b</span> called: 2 e8: 0000041b e8: 0000041b ...[생략]... called: 28 e8: 0000041b called: 29 e8: 0000041b called: 30 <span style='color: blue; font-weight: bold'>e9: 0000392b</span> called: 31 </pre> <br /> 보면 다음과 같은 공통점이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1번째 호출: e9 0000108b 2번째 ~ 29번째 호출: e8 0000041b 30번째 호출: e9 0000392b </pre> <br /> 실제로 저 단계별로 windbg에서 점프 위치에 대한 값을 확인해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [1번째 호출] 0:010> <span style='color: blue; font-weight: bold'>u 00007ff7`ad9710a0</span> 00007ff7`ad9710a0 e98b100000 <span style='color: blue; font-weight: bold'>jmp 00007ff7`ad972130</span> ...[생략]... [2번째 ~ 29번째 호출] 0:010> <span style='color: blue; font-weight: bold'>u 00007ff7`ad9710a0</span> 00007ff7`ad9710a0 e8fbd4ab5f <span style='color: blue; font-weight: bold'>call coreclr!PrecodeFixupThunk (00007ff8`0d42e5a0)</span> ...[생략]... [30번째 호출] 0:010> <span style='color: blue; font-weight: bold'>u 00007ff7`ad9710a0</span> 00007ff7`ad9710a0 e96b340000 <span style='color: blue; font-weight: bold'>jmp 00007ff7`ad974510</span> ...[생략]... </pre> <br /> 위의 결과를 바탕으로 대략 결론이 유추됩니다. 처음 메서드 호출 시 Tier-0 JIT를 빠르게 한 다음 그 이후의 호출에서는 최적화 JIT을 하기보다는 최적화를 할 조건을 갖출 때까지의 판단 코드가 있는 PrecodeFixupThunk를 경유해서 호출이 되다가 30번째 호출이 되었을 때 비로소 Tier-1 JIT를 하게 되는 것입니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1266&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> (2024-04-04: 업데이트)<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='pgo_tier_instrument_1.png' src='/SysWebRes/bbs/pgo_tier_instrument_1.png' /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1480
(왼쪽의 숫자를 입력해야 합니다.)