C# 변수, CPU, 그리고 LLM — `int age = 25;`에서 실리콘까지

발행: (2025년 12월 8일 오전 09:08 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

C# 변수, CPU, 그리고 LLM — int age = 25;에서 실리콘까지

대부분의 개발자는 “변수가 무엇인지” 알고 있다:

int age = 25;
string name = "Alice";
bool isStudent = true;

하지만 과학적인 정확성으로 답할 수 있는 사람은 거의 없다:

  • 실제로 컴파일러에서는 이 변수들이 어떻게 처리되는가?
  • 이 변수들은 어디에 존재하는가: 레지스터, 스택, 힙?
  • JIT는 어떻게 그 결정을 내리는가?
  • 이 내용이 성능과 대형 언어 모델(LLM)과의 대화에 왜 중요한가?

이 글에서는 작은 C# 예제를 사용해 시스템 수준의 정신 모델을 구축하고, 이를 ChatGPT, Claude 등 LLM에 더 나은 질문을 하는 방법과 연결한다.

코드 자체를 컴파일러 엔지니어처럼 진정으로 이해하고, 그 이해를 LLM에게 가르쳐 더 높은 수준에서 도움을 받길 원하는 사람이라면 이 글이 딱이다.

1. 정신 모델: C# 소스에서 CPU 전자까지

다음은 변수를 C#에서 볼 때마다 기억해야 할 핵심 파이프라인이다:

  1. C# 컴파일러(Roslyn) 가 코드를 IL(Intermediate Language) 로 변환한다.
  2. JIT 컴파일러(런타임) 가 그 IL을 CPU용 머신 코드 로 변환한다.
  3. CLR 런타임 이 그 변수의 “거주지” 를 결정한다:
    • 레지스터(빠르고 CPU 내부)
    • 스택 슬롯(콜 스택 프레임의 일부)
    • 힙에 있는 객체 내부 필드
  4. CPU 가 최종적으로 레지스터와 메모리의 전기 신호 를 조작한다.

🔎 변수 라는 단어는 언어 수준에서만 존재한다.
CPU 수준에서는 레지스터, 주소, 비트 만 존재한다.

LLM에게 시스템 엔지니어처럼 답을 얻고 싶다면, “C# 변수” 라는 표현만이 아니라 이 파이프라인에 대해 이야기해야 한다.

2. 예제: VariablesDeepDive.cs

레포지토리 안에 다음 파일이 있다고 가정해 보자:

// File: VariablesDeepDive.cs
// Author: Cristian Sifuentes Covarrubia + ChatGPT (Deep dive into C# variables)
// Goal: Explain variables like a systems / compiler / performance engineer.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

partial class Program
{
    static void VariablesDeepDive()
    {
        int age = 25;
        string name = "Alice";
        bool isStudent = true;

        Console.WriteLine($"Name: {name} is {age} years old and student status is {isStudent}");

        VariablesIntro();
        ValueVsReference();
        StackAndHeapDemo();
        RefAndInParameters();
        SpanAndPerformance();
        ClosuresAndCaptures();
        VolatileAndMemoryModel();
    }

    // ------------------------------------------------------------------------
    // 1. BASIC VARIABLES – BUT WITH A LOW-LEVEL VIEW
    // ------------------------------------------------------------------------
    static void VariablesIntro()
    {
        // At C# level:
        int age = 25;
        string name = "Alice";
        bool isStudent = true;

        Console.WriteLine($"[Intro] Name: {name} is {age} years old and student status is {isStudent}");

        // WHAT ACTUALLY HAPPENS?
        //
        // C# compiler (Roslyn):
        //   - Emits IL roughly like:
        //         .locals init (
        //             [0] int32 V_0,   // age
        //             [1] string V_1,  // name
        //             [2] bool V_2)   // isStudent
        //
        // JIT compiler:
        //   - Tries to map these locals to CPU registers when possible.
        //   - Might "spill" them to the stack if registers are insufficient.
        //
        // STACK vs REGISTERS:
        //   - `int age = 25;` might never live in memory at all:
        //       the JIT can load the constant 25 directly into a register.
        //   - If the JIT needs the value across instructions and lacks registers,
        //       it stores it in a stack slot (part of the stack frame).
        //
        // STRING "Alice":
        //   - `string` is a reference type.
        //   - The reference (pointer) is stored as a local variable
        //     (likely in a register or stack slot).
        //   - The actual characters live on the managed **heap**, allocated by the runtime.
        //
        // BOOL isStudent:
        //   - In IL it's a `bool` (System.Boolean), often compiled to a single byte.
        //   - In registers it's just bits; in memory it occupies at least one byte.
    }

    // ------------------------------------------------------------------------
    // 2. VALUE TYPES vs REFERENCE TYPES (STACK vs HEAP – BUT NOT ALWAYS)
    // ------------------------------------------------------------------------
    static void ValueVsReference()
    {
        // VALUE TYPE EXAMPLE
        // ------------------
        // struct is a value type. Its data is usually stored "inline"
        // (in the stack frame, in a register, or inside another object).
        PointStruct ps = new PointStruct { X = 10, Y = 20 };

        // REFERENCE TYPE EXAMPLE
        // ----------------------
        // class is a reference type. The variable holds a *reference* (pointer)
        // to an object on the heap.
        PointClass pc = new PointClass { X = 10, Y = 20 };

        Console.WriteLine($"[ValueVsReference] Struct: ({ps.X},{ps.Y}) | Class: ({pc.X},{pc.Y})");

        // LOW LEVEL NOTES:
        //   - `PointStruct ps`:
        //       IL has a local of type PointStruct.
        //       The struct fields X, Y are part of that local’s memory.
        //       CPU can load them from a stack slot or register.
        //
        //   - `PointClass pc`:
        //       `pc` itself is a 64‑bit reference (on a 64‑bit runtime).
        //       The real data (X, Y) resides on the heap.
        //       Access pattern: load reference → follow pointer → load fields.
        //
        // PERFORMANCE IMPLICATION:
        //   - Value types avoid an extra pointer indirection and allocation,
        //     but copying them can be expensive if the struct is large.
        //   - Reference types incur a heap allocation, pointer indirection,
        //     and GC tracking, but copying is cheap (just copy the reference).
    }

    struct PointStruct
    {
        public int X;
        public int Y;
    }

    class PointClass
    {
        public int X;
        public int Y;
    }

    // ------------------------------------------------------------------------
    // 3. STACK AND HEAP DEMONSTRATION (ESCAPE ANALYSIS)
    // ------------------------------------------------------------------------
    static void StackAndHeapDemo()
    {
        // Local variable that does NOT escape the method → can stay on the stack.
        int localValue = 42;

        // Variable that escapes (captured by a lambda) → heap allocation.
        Func<int> escaped = () => localValue;

        Console.WriteLine($"[StackAndHeapDemo] escaped() = {escaped()}");
    }

    // ------------------------------------------------------------------------
    // 4. REF, IN, AND SPAN (PERFORMANCE‑ORIENTED THINKING)
    // ------------------------------------------------------------------------
    static void RefAndInParameters()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        SumRef(ref numbers[0]);          // passes by reference, JIT may keep it in a register
        SumIn(in numbers[0]);            // read‑only reference, helps avoid copies
    }

    static void SumRef(ref int value)
    {
        value += 10;
    }

    static void SumIn(in int value)
    {
        // value is read‑only; JIT can treat it like a normal local
        int result = value + 10;
        Console.WriteLine($"[SumIn] result = {result}");
    }

    // ------------------------------------------------------------------------
    // 5. SPAN AND MEMORY‑EFFICIENCY
    // ------------------------------------------------------------------------
    static void SpanAndPerformance()
    {
        Span<int> slice = stackalloc int[3] { 10, 20, 30 };
        for (int i = 0; i < slice.Length; i++)
        {
            Console.WriteLine($"[Span] slice[{i}] = {slice[i]}");
        }
    }

    // ------------------------------------------------------------------------
    // 6. CLOSURES AND CAPTURES
    // ------------------------------------------------------------------------
    static void ClosuresAndCaptures()
    {
        int counter = 0;
        Action increment = () => counter++;
        increment();
        increment();
        Console.WriteLine($"[Closures] counter = {counter}");
    }

    // ------------------------------------------------------------------------
    // 7. VOLATILE AND THE MEMORY MODEL
    // ------------------------------------------------------------------------
    static void VolatileAndMemoryModel()
    {
        // Demonstrates the use of the volatile keyword.
        // In real multi‑core scenarios, volatile ensures reads/writes
        // are not reordered across threads.
        volatile int flag = 0;
        // ... imagine another thread sets flag = 1;
        if (flag == 1)
        {
            Console.WriteLine("[Volatile] Flag observed as 1");
        }
    }
}

핵심 정리

  • 변수는 오직 소스 언어 수준에만 존재한다; 런타임은 이를 레지스터, 스택 슬롯, 혹은 힙 위치에 매핑한다.
  • 값 타입은 보통 인라인으로 저장되고, 참조 타입은 힙에 할당된 데이터에 대한 포인터를 저장한다.
  • JIT 최적화(레지스터 할당, 스필링, 이스케이프 분석)는 실제 저장 위치를 런타임에 결정한다.
  • 이 파이프라인을 이해하면 더 성능 좋은 코드를 작성할 수 있을 뿐 아니라, LLM에 “왜 JIT가 이 로컬 변수를 스택에 스필링했나요?”처럼 정확한 질문을 할 수 있다.
Back to Blog

관련 글

더 보기 »