· 5 min read

C# 람다 내부 구현

C# 람다 식을 컴파일러가 클래스와 메서드로 변환하는 과정을 IL 코드와 함께 분석한다. 클로저, 캡처 변수, 성능 영향까지.

마법은 없다. Lambda를 컴파일러가 어떤식으로 구현하는지 알아둘 필요가 있다.

public static class LambdaExample
{
    public static void Run()
    {
        int localInt = 123456789;
        string localString = "LocalString";
        ActionRunner(() =>
        {
            Console.WriteLine($"{localInt}, {localString}");
        });
    }

    private static void ActionRunner(Action action)
    {
        action?.Invoke();
    }
}
Decompiled il source
  
    // Type: CSharpExamples.LambdaExample 
    // Assembly: CSharpExamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    // MVID: B420F7F8-9269-4798-8719-2A8ECDC7FBCA
    // Location: /Users/geunheepark/Projects/private/CSharpExamples/CSharpExamples/bin/Debug/netcoreapp3.1/CSharpExamples.dll
    // Sequence point data from /Users/geunheepark/Projects/private/CSharpExamples/CSharpExamples/bin/Debug/netcoreapp3.1/CSharpExamples.pdb

    .class public abstract sealed auto ansi beforefieldinit
      CSharpExamples.LambdaExample
        extends [System.Runtime]System.Object
    {

      .class nested private sealed auto ansi beforefieldinit
        '<>c__DisplayClass0_0'
          extends [System.Runtime]System.Object
      {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
          = (01 00 00 00 )

        .field public int32 localInt

        .field public string localString

        .method public hidebysig specialname rtspecialname instance void
          .ctor() cil managed
        {
          .maxstack 8

          IL_0000: ldarg.0      // this
          IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
          IL_0006: nop
          IL_0007: ret

        } // end of method '<>c__DisplayClass0_0'::.ctor

        .method assembly hidebysig instance void
          'Runb__0'() cil managed
        {
          .maxstack 8

          // [12 13 - 12 14]
          IL_0000: nop

          // [13 17 - 13 65]
          IL_0001: ldstr        "{0}, {1}"
          IL_0006: ldarg.0      // this
          IL_0007: ldfld        int32 CSharpExamples.LambdaExample/'<>c__DisplayClass0_0'::localInt
          IL_000c: box          [System.Runtime]System.Int32
          IL_0011: ldarg.0      // this
          IL_0012: ldfld        string CSharpExamples.LambdaExample/'<>c__DisplayClass0_0'::localString
          IL_0017: call         string [System.Runtime]System.String::Format(string, object, object)
          IL_001c: call         void [System.Console]System.Console::WriteLine(string)
          IL_0021: nop

          // [14 13 - 14 14]
          IL_0022: ret

        } // end of method '<>c__DisplayClass0_0'::'Runb__0'
      } // end of class '<>c__DisplayClass0_0'

      .method public hidebysig static void
        Run() cil managed
      {
        .maxstack 2
        .locals init (
          [0] class CSharpExamples.LambdaExample/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
        )

        IL_0000: newobj       instance void CSharpExamples.LambdaExample/'<>c__DisplayClass0_0'::.ctor()
        IL_0005: stloc.0      // 'CS$<>8__locals0'

        // [8 9 - 8 10]
        IL_0006: nop

        // [9 13 - 9 38]
        IL_0007: ldloc.0      // 'CS$<>8__locals0'
        IL_0008: ldc.i4       123456789 // 0x075bcd15
        IL_000d: stfld        int32 CSharpExamples.LambdaExample/'<>c__DisplayClass0_0'::localInt

        // [10 13 - 10 48]
        IL_0012: ldloc.0      // 'CS$<>8__locals0'
        IL_0013: ldstr        "LocalString"
        IL_0018: stfld        string CSharpExamples.LambdaExample/'<>c__DisplayClass0_0'::localString

        // [11 13 - 14 16]
        IL_001d: ldloc.0      // 'CS$<>8__locals0'
        IL_001e: ldftn        instance void CSharpExamples.LambdaExample/'<>c__DisplayClass0_0'::'Runb__0'()
        IL_0024: newobj       instance void [System.Runtime]System.Action::.ctor(object, native int)
        IL_0029: call         void CSharpExamples.LambdaExample::ActionRunner(class [System.Runtime]System.Action)
        IL_002e: nop

        // [15 9 - 15 10]
        IL_002f: ret

      } // end of method LambdaExample::Run

      .method private hidebysig static void
        ActionRunner(
          class [System.Runtime]System.Action action
        ) cil managed
      {
        .maxstack 8

        // [18 9 - 18 10]
        IL_0000: nop

        // [19 13 - 19 30]
        IL_0001: ldarg.0      // action
        IL_0002: brtrue.s     IL_0006
        IL_0004: br.s         IL_000d
        IL_0006: ldarg.0      // action
        IL_0007: callvirt     instance void [System.Runtime]System.Action::Invoke()
        IL_000c: nop

        // [20 9 - 20 10]
        IL_000d: ret

      } // end of method LambdaExample::ActionRunner
    } // end of class CSharpExamples.LambdaExample
  

위의 코드는 아래와 같이 해석될 수 있다.

public static class LambdaExampleTranslated
{
    public class LambdaClass
    {
        public int localInt;
        public string localString;

        public void Run()
        {
            Console.WriteLine($"{localInt}, {localString}");
        }
    }

    public static void Run()
    {
        int localInt = 123456789;
        string localString = "LocalString";
        ActionRunner(new LambdaClass
		{
			localInt = localInt,
			localString = localString
		}
		.Run);
    }

    private static void ActionRunner(Action action)
    {
        action?.Invoke();
    }
}



Lambda의 변수 Capture를 위해 Lambda 객체는 Capture할 변수들을 멤버로 지니게 된다.

→ Capture할 변수가 많으면 많을수록, Lambda 객체 자체의 Size도 커진다.

매 Lambda 식 전달마다, Lambda 클래스 객체가 생성되고 사용후 버려지기 때문에 GC Heap에 부담을 준다.

→ 빈번한 GC 발생은 Program performance 에 악영향을 끼침.

그러므로 performance- critical 한 Code에서는 Lambda 를 대체하여 최적화를 할 필요가 있다.

// Lambda 객체 10000회 생성!
for (int i = 0; i < 10000; ++i)
{
    ActionRunner(() =>
    {
        Console.WriteLine($"{localInt}, {localString}");
    });
}

// Lambda 객체 1회 생성!
Action action = () =>
{
    Console.WriteLine($"{localInt}, {localString}");
};
for (int i = 0; i < 10000; ++i)
{
    ActionRunner(action);
}



Method → Delegate 변환 Overhead

Method를 Delegate로 전달시 Action등 새로운 Delegate 객체가 생성되고, 그것이 전달된다.

→ 생성 과정에서 GC Alloc, 사용 이후엔 Garbage가 되는 Overhead가 있기 때문에 조심해야 한다.

public class DelegateConversion
{
    public void Run()
    {
        // Action 10000회 생성.
        for (int i = 0; i < 10000; ++i)
            ActionRunner(MemberMethod);

        // Action 1회 생성.
        Action action = MemberMethod;
        for (int i = 0; i < 10000; ++i)
            ActionRunner(action);
    }

    private static void ActionRunner(Action action)
    {
        action?.Invoke();
    }

    private void MemberMethod() { }
}

❇️ Dash/Net/SendQueue.cs 에선 Delegate 캐싱 최적화를 통해 Overhead를 줄였음.

public class SendQueue
{
	private readonly IEventLoop _eventLoop;
	// 미리 Delegate를 캐싱하여 GC Alloc 회피.
	private readonly Action _eventLoopAction = null;
	...
	public SendQueue()
	{
		_eventLoopAction = OnEventLoop;
	}

	public void Send()
	{
		...
		_eventLoop.Execute(_eventLoopAction);
		...
	}
	public void OnEventLoop() { ... }
}


❗컴파일러 / CLR 버전 / 실행 환경에 따라 최적화등으로 인해 결과가 상이할 수 있다.

Related Posts

View All Posts »

.NET 서버 메모리 누수 잡기 — dotnet-gcdump로 안 보이는 할당 폭탄

.NET 서버 메모리가 2분 만에 136MB에서 14GB로 폭증했다. dotnet-gcdump와 dotnet-counters로도 안 잡히는 할당 폭탄을 추적하고, 그 과정을 재사용 가능한 스킬로 만든 이야기.

#Aethelgard #AI #Claude #GameDev
Unity로 VR 악기 앱 만들기 — 루프 스테이션 1인 밴드 (Solo Band Studio)

Unity로 VR 악기 앱 만들기 — 루프 스테이션 1인 밴드 (Solo Band Studio)

Meta Quest용 VR 음악 창작 툴을 Unity로 개발한 과정. 피아노·드럼·베이스를 연주하고 루프 스테이션으로 혼자 합주를 완성한다. AI와 함께 한 달 만에 만든 대학 수업 프로젝트.

#Unity #VR #Audio #CSharp
실시간 멀티플레이 게임 아키텍처 회고 — 루니아 원정대 5년 후

실시간 멀티플레이 게임 아키텍처 회고 — 루니아 원정대 5년 후

모바일 실시간 멀티플레이 게임 루니아 원정대의 서버 5종 + 클라이언트 2종 전체 아키텍처를 설계했던 경험. 커스텀 ECS, 커맨드 동기화, 분산 서버, 재접속 시스템까지 — 5년이 지난 지금 그때의 기술 판단들을 돌아본다.

#Architecture #Retrospective #CSharp #Multiplay