C# 숫자형 기본 개념 — 정수부터 SIMD까지, LLM-Ready Thinking을 위해

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

Source: Dev.to

Basic Concepts of C# Numeric Types — From Integers to SIMD, for LLM-Ready Thinking

대부분의 C# 개발자는 매일 숫자형 타입을 사용합니다:

int, long, float, double, decimal

보다 깊은, 정밀도와 성능에 초점을 맞춘 질문을 시작하면 상황이 흥미로워집니다:

  • int가 오버플로우될 때 실제로 무슨 일이 일어날까요?
  • double0.1 + 0.2가 정확히 0.3이 아닌 이유는?
  • 실제 시스템에서 double 대신 decimal을 언제 사용해야 할까요?
  • 리터럴 접미사(f, m, L)가 IL과 JIT 동작을 어떻게 바꾸나요?
  • Vector를 사용해 숫자 연산을 SIMD으로 끌어올리려면?
  • LLM에게 숫자형 타입에 대해 어떻게 이야기하면 성능 엔지니어처럼 추론할 수 있을까요?

이 글에서는 숫자형 타입을 전체 스택 개념으로 다루는 ShowNumericTypes() 모듈을 살펴봅니다: Roslyn, IL, JIT, CPU, SIMD까지. 그리고 그 정신 모델을 LLM을 위한 더 나은 프롬프트와 연결해, 얕은 답변이 아닌 프로덕션 수준의 답변을 얻을 수 있게 합니다.

콘솔 앱을 실행할 수 있다면 따라 할 수 있습니다.


1. 정신 모델: 숫자형 타입이 .NET 스택을 통해 흐르는 방식

C# 소스RoslynILJITCPU

int x = 42;
double y = 3.1416;
  • Roslyn은 소스를 구체적인 스택 타입(int32, float64, valuetype System.Decimal, …)을 가진 IL로 컴파일합니다.
  • JIT는 IL을 기계어 코드로 변환하면서 어떤 레지스터를 사용할지(일반 목적 레지스터 vs 부동소수점/SIMD)와 오버플로우 체크(add.ovf)를 삽입할지 결정합니다.
  • CPU는 결과 명령을 실행합니다:
    • 정수 ALU(add, imul, …)
    • 부동소수점/SIMD(addss, addps, vmulps, …)
    • decimal을 위한 다중 워드 정수 시퀀스

핵심 아이디어int, double, decimal은 여러분의 코드, CLR, JIT, CPU 사이의 계약이며, 단순히 “C#의 타입”이 아닙니다.

LLM에게 도움을 요청할 때는 이 파이프라인을 기준으로 질문을 구성하세요: Roslyn → IL → JIT → CPU.


2. 데모 파일: ShowNumericTypes() 개요

partial class Program
{
    // Call ShowNumericTypes() from your own Main() in another partial Program.
    static void ShowNumericTypes()
    {
        var integerNumber   = 42m;
        double doubleNumber = 3.1416d;
        float floatingNumber = 274f;
        long longNumber     = 300_200_100L;
        decimal monetaryNumber = 99.99m;

        Console.WriteLine($"Entero: {integerNumber}");
        Console.WriteLine($"Double: {doubleNumber}");
        Console.WriteLine($"Float: {floatingNumber}");
        Console.WriteLine($"Long: {longNumber}");
        Console.WriteLine($"Decimal: {monetaryNumber}");

        BasicNumericTypesIntro();
        IntegerRangeAndOverflow();
        FloatingPointPrecision();
        DecimalForMoney();
        NumericLiteralsAndTypeInference();
        VectorizationAndSIMD();
    }
}

각 보조 메서드는 숫자 이야기에 대한 한 부분에 초점을 맞춘 “실험실”입니다. 이들을 합치면 공개 GitHub 저장소에 넣어 LLM과 대화할 때 재사용할 수 있는 교육 파일이 됩니다.


3. 기본 숫자형 타입: int, long, float, double, decimal

static void BasicNumericTypesIntro()
{
    int    integerNumber  = 42;              // System.Int32
    double doubleNumber   = 3.1416d;         // System.Double
    float  floatingNumber = 274f;            // System.Single
    long   longNumber     = 300_200_100L;    // System.Int64
    decimal monetaryNumber = 99.99m;         // System.Decimal

    Console.WriteLine($"[Basic]   Int:     {integerNumber}");
    Console.WriteLine($"[Basic]   Double:  {doubleNumber}");
    Console.WriteLine($"[Basic]   Float:   {floatingNumber}");
    Console.WriteLine($"[Basic]   Long:    {longNumber}");
    Console.WriteLine($"[Basic]   Decimal: {monetaryNumber}");
}

개념적 IL

.locals init (
    [0] int32   integerNumber,
    [1] float64 doubleNumber,
    [2] float32 floatingNumber,
    [3] int64   longNumber,
    [4] valuetype [System.Runtime]System.Decimal monetaryNumber
)

CPU 매핑

C# 타입일반적인 레지스터
int / long범용 정수 레지스터 (EAX/RAX/RCX/…)
float / double부동소수점 / SIMD 레지스터 (XMM/YMM)
decimal소프트웨어로 구현되는 다중 32‑bit 정수 연산

decimal은 이진 부동소수점에 대한 하드웨어가 존재하지만, 10진수 연산을 위한 하드웨어가 없기 때문에 double보다 비용이 많이 듭니다.


4. 정수 범위와 오버플로우: checked vs unchecked

static void IntegerRangeAndOverflow()
{
    int max = int.MaxValue;
    int min = int.MinValue;

    Console.WriteLine($"[IntRange] int.MinValue = {min}, int.MaxValue = {max}");

    int overflowUnchecked = unchecked(max + 1);
    Console.WriteLine($"[Overflow] unchecked(max + 1) = {overflowUnchecked}");

    try
    {
        int overflowChecked = checked(max + 1);
        Console.WriteLine($"[Overflow] checked(max + 1) = {overflowChecked}");
    }
    catch (OverflowException ex)
    {
        Console.WriteLine($"[Overflow] checked(max + 1) threw: {ex.GetType().Name}");
    }
}
  • int는 32‑bit 2의 보수 값이며, 범위는 [-2^31, 2^31‑1]입니다.
  • int.MaxValue = 0x7FFFFFFF (2,147,483,647). 여기에 1을 더하면 0x80000000 (‑2,147,483,648)으로 래핑됩니다.

IL 명령어

Opcode동작
add체크 안 함 – 오버플로우 시 래핑
add.ovf체크 함 – 오버플로우 시 OverflowException 발생

설계 규칙

  • 보안‑중요하거나 재무 연산에서는 checked를 사용해 버그를 조기에 잡아냅니다.
  • 오버플로우가 불가능하거나 의도적으로 허용되는 경우(예: 해시 함수)에는 핫 경로에서 unchecked를 사용합니다.

LLM 프롬프트 예시

IntegerRangeAndOverflow() 메서드에 대해 IL(add vs add.ovf)을 설명하고, x86‑64 어셈블리와 CPU 플래그가 오버플로우를 어떻게 감지하는지 보여 주세요.”


5. 부동소수점 정밀도: IEEE‑754와 비트 패턴

static void FloatingPointPrecision()
{
    double a = 0.1;
    double b = 0.2;
    double c = a + b;

    Console.WriteLine($"[FP] 0.1 + 0.2 = {c:R}  (R = round‑trip format)");

    long rawBits = BitConverter.DoubleToInt64Bits(c);
    Console.WriteLine($"[FP] Bits of (0.1+0.2): 0x{rawBits:X16}");

    float  fx = 1f / 10f;
    double dx = 1d / 10d;
    Console.WriteLine($"[FP] 1/10 as float = {fx:R}");
    Console.WriteLine($"[FP] 1/10 as double = {dx:R}");
}
  • double은 IEEE‑754 binary64를 따릅니다. 0.10.2는 정확히 표현될 수 없으므로, 합산 시 작은 반올림 오차가 발생합니다.
  • R 포맷 지정자는 라운드‑트립 표현을 출력해, 다시 파싱하면 원래 바이너리 값을 얻을 수 있음을 보장합니다.
  • BitConverter.DoubleToInt64Bits는 기본 64‑bit 패턴(0x3FD999999999999A for 0.1 + 0.2)을 보여줍니다.

6. Decimal: 금전을 위한 10진수 연산

static void DecimalForMoney()
{
    decimal price = 19.95m;
    decimal taxRate = 0.07m;      // 7 %
    decimal total = price + price * taxRate;

    Console.WriteLine($"[Decimal] Price: {price:C}");
    Console.WriteLine($"[Decimal] Tax (7 %): {(price * taxRate):C}");
    Console.WriteLine($"[Decimal] Total: {total:C}");
}
  • decimal은 96‑bit 정수 데이터와 스케일링 팩터(0‑28)를 저장해, 최대 28자리 정밀도를 정확한 10진수 형태로 제공합니다.
  • 반올림 오류를 허용할 수 없는 재무 계산에 이상적입니다.
  • 단점은 CLR이 여러 32‑bit 정수 연산을 사용해 소프트웨어로 구현하기 때문에 연산 속도가 느리다는 점입니다.

7. 숫자 리터럴 & 타입 추론: 접미사가 IL을 바꾸는 방식

static void NumericLiteralsAndTypeInference()
{
    var i = 42;      // int (int32)
    var l = 42L;     // long (int64)
    var f = 42f;     // float (float32)
    var d = 42d;     // double (float64)
    var m = 42m;     // decimal (valuetype System.Decimal)

    Console.WriteLine($"[Literals] int: {i.GetType()}, long: {l.GetType()}, float: {f.GetType()}, double: {d.GetType()}, decimal: {m.GetType()}");
}
  • 접미사(L, f, d, m)는 컴파일러가 해당 IL 타입을 내보내도록 강제합니다.
  • 접미사가 없으면 정수 리터럴은 기본적으로 int(값이 int.MaxValue를 초과하면 long)이 됩니다.
  • 부동소수점 리터럴은 기본적으로 double입니다.

IL 예시 (var f = 42f; 경우)

ldc.r4 42.0
stloc.0

반면 42ldc.i4.s 42 로 컴파일됩니다.


8. 벡터화 & SIMD: Vector를 이용한 실제 성능 향상

using System.Numerics;

static void VectorizationAndSIMD()
{
    // Simple element‑wise addition of two float arrays using SIMD
    float[] a = { 1, 2, 3, 4, 5, 6, 7, 8 };
    float[] b = { 8, 7, 6, 5, 4, 3, 2, 1 };
    float[] result = new float[a.Length];

    int vectorSize = Vector<float>.Count; // typically 4 or 8 depending on hardware
    int i = 0;

    for (; i (a, i);
        var vb = new Vector<float>(b, i);
        (va + vb).CopyTo(result, i);
    }

    // Handle remaining elements
    for (; i < a.Length; i++)
    {
        result[i] = a[i] + b[i];
    }
}
  • Vector<T>는 하드웨어 SIMD 레지스터(SSE, AVX 등)를 추상화합니다.
  • JIT는 대상 CPU가 지원하면 (addps, addpd, …)와 같은 벡터 명령을 생성합니다.
  • 데이터 양이 충분히 커서 벡터 로드/스토어 오버헤드를 상쇄할 수 있는 경우에 이 패턴을 사용해 뜨거운 수치 루프를 최적화합니다.

9. 이 정신 모델을 활용해 LLM에서 더 많은 정보를 얻는 방법

성능이나 정확성에 대해 LLM에게 묻고 싶을 때:

  1. 관심 레이어를 명시하세요 (source → IL → JIT → CPU).
  2. 관련 코드 스니펫을 제공하고 IL이나 어셈블리를 요청하세요.
  3. 트레이드‑오프 분석을 요구하세요 (예: “10 M 아이템을 집계할 때 floatdecimal을 비교해 주세요”).
  4. 마이크로‑벤치마크 아이디어를 요청해 JIT‑생성 코드를 격리하도록 하세요.

프롬프트 예시

VectorizationAndSIMD() 메서드에 대해 AVX2가 지원되는 x86‑64 머신에서 JIT가 생성한 어셈블리를 보여 주시고, CPU의 벡터 레지스터가 어떻게 사용되는지 설명해 주세요.”


10. 숫자형 타입 마스터 체크리스트 (상위 1 % 개발자 마인드셋)

  • 전체 스택(source → Roslyn → IL → JIT → CPU)을 이해한다.
  • checkedunchecked가 언제 필요한지, 그리고 생성되는 IL 명령을 안다.
  • float/double의 IEEE‑754 비트 패턴을 읽고 해석할 수 있다.
  • 정확한 10진수 연산이 필요할 때 decimal을 선택하고, 그 성능 비용을 인식한다.
  • 리터럴 접미사를 사용해 원하는 IL 타입을 제어한다.
  • 데이터‑병렬 워크로드에 Vector 또는 하드웨어 인트린식을 적용한다.
  • 특정 파이프라인 단계에 초점을 맞춘 LLM 프롬프트를 만든다.

이 지식을 갖추면 숫자적으로 견고한 C# 코드를 작성할 뿐 아니라 LLM에게도 정확하고 성능‑인식적인 답변을 얻을 수 있습니다.

Back to Blog

관련 글

더 보기 »