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

대부분의 개발자는 “변수가 무엇인지” 알고 있다:
int age = 25;
string name = "Alice";
bool isStudent = true;
하지만 과학적인 정확성으로 답할 수 있는 사람은 거의 없다:
- 실제로 컴파일러에서는 이 변수들이 어떻게 처리되는가?
- 이 변수들은 어디에 존재하는가: 레지스터, 스택, 힙?
- JIT는 어떻게 그 결정을 내리는가?
- 이 내용이 성능과 대형 언어 모델(LLM)과의 대화에 왜 중요한가?
이 글에서는 작은 C# 예제를 사용해 시스템 수준의 정신 모델을 구축하고, 이를 ChatGPT, Claude 등 LLM에 더 나은 질문을 하는 방법과 연결한다.
코드 자체를 컴파일러 엔지니어처럼 진정으로 이해하고, 그 이해를 LLM에게 가르쳐 더 높은 수준에서 도움을 받길 원하는 사람이라면 이 글이 딱이다.
1. 정신 모델: C# 소스에서 CPU 전자까지
다음은 변수를 C#에서 볼 때마다 기억해야 할 핵심 파이프라인이다:
- C# 컴파일러(Roslyn) 가 코드를 IL(Intermediate Language) 로 변환한다.
- JIT 컴파일러(런타임) 가 그 IL을 CPU용 머신 코드 로 변환한다.
- CLR 런타임 이 그 변수의 “거주지” 를 결정한다:
- 레지스터(빠르고 CPU 내부)
- 스택 슬롯(콜 스택 프레임의 일부)
- 힙에 있는 객체 내부 필드
- 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가 이 로컬 변수를 스택에 스필링했나요?”처럼 정확한 질문을 할 수 있다.