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

대부분의 C# 개발자는 매일 숫자형 타입을 사용합니다:
int, long, float, double, decimal
보다 깊은, 정밀도와 성능에 초점을 맞춘 질문을 시작하면 상황이 흥미로워집니다:
int가 오버플로우될 때 실제로 무슨 일이 일어날까요?double로0.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# 소스 → Roslyn → IL → JIT → CPU
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을 위한 다중 워드 정수 시퀀스
- 정수 ALU(
핵심 아이디어 –
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(addvsadd.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.1과0.2는 정확히 표현될 수 없으므로, 합산 시 작은 반올림 오차가 발생합니다.R포맷 지정자는 라운드‑트립 표현을 출력해, 다시 파싱하면 원래 바이너리 값을 얻을 수 있음을 보장합니다.BitConverter.DoubleToInt64Bits는 기본 64‑bit 패턴(0x3FD999999999999Afor0.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
반면 42는 ldc.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에게 묻고 싶을 때:
- 관심 레이어를 명시하세요 (source → IL → JIT → CPU).
- 관련 코드 스니펫을 제공하고 IL이나 어셈블리를 요청하세요.
- 트레이드‑오프 분석을 요구하세요 (예: “10 M 아이템을 집계할 때
float와decimal을 비교해 주세요”). - 마이크로‑벤치마크 아이디어를 요청해 JIT‑생성 코드를 격리하도록 하세요.
프롬프트 예시
“
VectorizationAndSIMD()메서드에 대해 AVX2가 지원되는 x86‑64 머신에서 JIT가 생성한 어셈블리를 보여 주시고, CPU의 벡터 레지스터가 어떻게 사용되는지 설명해 주세요.”
10. 숫자형 타입 마스터 체크리스트 (상위 1 % 개발자 마인드셋)
- 전체 스택(source → Roslyn → IL → JIT → CPU)을 이해한다.
-
checked와unchecked가 언제 필요한지, 그리고 생성되는 IL 명령을 안다. -
float/double의 IEEE‑754 비트 패턴을 읽고 해석할 수 있다. - 정확한 10진수 연산이 필요할 때
decimal을 선택하고, 그 성능 비용을 인식한다. - 리터럴 접미사를 사용해 원하는 IL 타입을 제어한다.
- 데이터‑병렬 워크로드에
Vector또는 하드웨어 인트린식을 적용한다. - 특정 파이프라인 단계에 초점을 맞춘 LLM 프롬프트를 만든다.
이 지식을 갖추면 숫자적으로 견고한 C# 코드를 작성할 뿐 아니라 LLM에게도 정확하고 성능‑인식적인 답변을 얻을 수 있습니다.