C# - JIT 컴파일러의 인라인 메서드 처리 유무
지난 글을 기준으로,
C# - 인라인 메서드(inline methods)
; https://www.sysnet.pe.kr/2/0/13063
실제로 인라인 여부를 확인해볼까요? ^^ 일반적인 Increment 메서드의 구현이라면,
static void Main(string[] args)
{
int x = Increment(args.Length);
Console.WriteLine(x);
Console.ReadLine();
}
static int Increment(int x)
{
if (x < 0)
{
ThrowArgumentException();
}
return (x + 1);
}
static int ThrowArgumentException()
{
throw new ArgumentException();
}
(Release 빌드로 실행해 ReadLine에 걸렸을 때 debug attach 시킨 후) disassembly 창으로 보면 이렇게 JIT 컴파일링 된 것을 확인할 수 있습니다.
00007FFDC0E02990 push rbp
00007FFDC0E02991 sub rsp,20h
00007FFDC0E02995 lea rbp,[rsp+20h]
00007FFDC0E0299A mov qword ptr [rbp+10h],rcx
00007FFDC0E0299E mov rcx,qword ptr [rbp+10h]
int x = Increment(args.Length);
00007FFDC0E029A2 mov ecx,dword ptr [rcx+8]
00007FFDC0E029A5 call CLRStub[MethodDescPrestub]@7ffdc0dff118 (07FFDC0DFF118h)
Console.WriteLine(x);
00007FFDC0E029AA mov ecx,eax
00007FFDC0E029AC call CLRStub[MethodDescPrestub]@7ffdc0e028e8 (07FFDC0E028E8h)
인라인이 안 되었죠? ^^ 아니... 인라인 시킨다면서요? 게다가 해당 메서드에 "[MethodImpl(MethodImplOptions.AggressiveInlining)]" 특성을 부여해도 결과는 마찬가지입니다.
왜냐하면, JIT 컴파일러는 위와 같은 상황에서 Increment 호출을 인라인 시킨다고 크게 성능상 장점이 없다고 판단하는 것 같습니다. 그래서 인라인을 위한 계산 비용보다는 그냥 함수 호출로 빠르게 JIT 번역해 버리는 것입니다.
오호~~~ 그렇다면, 성능 향상이 부각되는 상황으로 만들면 되겠군요. ^^ 그래서 임의로 다음과 같이 for 문을 구성해 봤습니다.
for (int i = 0; i < 100; i++)
{
int x = Increment(args.Length);
Console.WriteLine(x);
}
이렇게 만들면 이제 인라인이 됩니다.
int x = Increment(args.Length);
00007FFDC0E229AB mov ecx,edi
00007FFDC0E229AD test ecx,ecx
00007FFDC0E229AF jl Program.Main(System.String[])+02Ch (07FFDC0E229CCh)
00007FFDC0E229B1 inc ecx
Console.WriteLine(x);
00007FFDC0E229B3 call CLRStub[MethodDescPrestub]@7ffdc0e228e8 (07FFDC0E228E8h)
...[생략]...
ThrowArgumentException();
00007FFDC0E229CC call CLRStub[MethodDescPrestub]@7ffdc0e1f120 (07FFDC0E1F120h)
(참고로, 위의 경우 AggressiveInlining 옵션을 빼도 상관없습니다.)
그런데, 재미있는 점이 하나 있습니다. ^^; .NET 6 환경에서 위의 상황을, Increment에 throw를 포함하는 유형으로 테스트를 하면,
static int Increment(int x)
{
if (x < 0)
{
throw new ArgumentException();
}
return (x + 1);
}
그래도 여전히 인라인이 되는 것을 확인할 수 있습니다. 아니... 이게 어떻게 된 일입니까? ^^; "
.NET Core Best Practices" 글을 쓴 사람은 분명히 인라인이 안 된다고 했습니다. 어쩔 수 없습니다. 그냥 사실을 받아들여야 합니다. ^^ 어쨌든 (어느 버전인지는 일일이 테스트를 해보면 알겠지만) 적어도 .NET 6의 JIT 컴파일러는 throw 문을 포함해도 정상적으로 인라인 시킬 정도로 더 똑똑해진 것입니다.
그래서, 동일한 코드를 .NET Framework 4.8 런타임에서 실행해 보면 다시 이렇게 인라인이 안 되는 것을 볼 수 있습니다.
031E0848 push ebp
031E0849 mov ebp,esp
031E084B push edi
031E084C push esi
031E084D xor esi,esi
031E084F mov edi,dword ptr [ecx+4]
int x = Increment(args.Length);
031E0852 mov ecx,edi
031E0854 call dword ptr [Pointer to: Program.Increment(Int32) (03154D58h)]
Console.WriteLine(x);
031E085A mov ecx,eax
031E085C call System.Console.WriteLine(Int32) (6F931938h)
결국, 이런 내부적인 동작은 끊임없이 바뀔 것이고 특별한 경우가 아니라면 굳이 신경 쓸 필요는 없습니다. 단지, 우리는 그냥
free lunch를 즐기는 식으로 가볍게 지나가면 되겠습니다. ^^
그나저나, (적어도 .NET 6 환경에서는) "[MethodImpl(MethodImplOptions.AggressiveInlining)]" 특성은 이젠 거의 무시하는 분위기인 듯합니다. 왜냐하면, 그 옵션의 유무에 따른 변화가 딱히 없습니다. 혹시, 이 옵션에 따라 달라지는 상황이 있다면 덧글 부탁드립니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]