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

Most C# developers use numeric types every day:
int, long, float, double, decimal
When you start asking deeper, precision‑ and performance‑focused questions, things get interesting:
- What actually happens when an
intoverflows? - Why is
0.1 + 0.2not exactly0.3withdouble? - When should I use
decimalinstead ofdoublein real systems? - How do literal suffixes (
f,m,L) change IL and JIT behavior? - How can I push numeric operations into SIMD using
Vector? - How do I talk about numeric types with LLMs so they reason like performance engineers?
In this article we’ll walk through a ShowNumericTypes() module that treats numeric types as a full‑stack concept: from Roslyn and IL to JIT, CPU, and SIMD. Then we’ll connect that mental model to better prompts for LLMs so you can get production‑grade answers instead of shallow ones.
If you can run a console app, you can follow this.
1. Mental Model: How Numeric Types Flow Through the .NET Stack
C# source → Roslyn → IL → JIT → CPU
int x = 42;
double y = 3.1416;
- Roslyn compiles the source into IL with concrete stack types (
int32,float64,valuetype System.Decimal, …). - The JIT turns IL into machine code, deciding which registers to use (general‑purpose vs floating‑point/SIMD) and whether to insert overflow checks (
add.ovf). - The CPU executes the resulting instructions:
- Integer ALU (
add,imul, …) - Floating‑point/SIMD (
addss,addps,vmulps, …) - Multi‑word integer sequences for
decimal.
- Integer ALU (
Key idea –
int,double,decimalare contracts between your code, the CLR, the JIT, and the CPU, not just “types in C#”.
When you ask LLMs for help, frame questions in terms of this pipeline: Roslyn → IL → JIT → CPU.
2. The Demo File: ShowNumericTypes() Overview
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();
}
}
Each helper method is a “lab” focused on one part of the numeric story. Together they form a teaching file you can put into a public GitHub repo and reuse when talking with LLMs.
3. Basic Numeric Types: 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}");
}
Conceptual IL
.locals init (
[0] int32 integerNumber,
[1] float64 doubleNumber,
[2] float32 floatingNumber,
[3] int64 longNumber,
[4] valuetype [System.Runtime]System.Decimal monetaryNumber
)
CPU Mapping
| C# type | Typical register |
|---|---|
int / long | General‑purpose integer registers (EAX/RAX/RCX/…) |
float / double | Floating‑point / SIMD registers (XMM/YMM) |
decimal | Handled via multiple 32‑bit integer operations in software |
⚠
decimalis more expensive thandoublebecause the CPU has native hardware for binary floating point but not for base‑10 decimal arithmetic.
4. Integer Range & Overflow: 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}");
}
}
intis a 32‑bit two’s‑complement value, range[-2^31, 2^31‑1].int.MaxValue=0x7FFFFFFF(2,147,483,647). Adding 1 wraps to0x80000000(‑2,147,483,648).
IL opcodes
| Opcode | Behaviour |
|---|---|
add | Unchecked – wraps on overflow |
add.ovf | Checked – throws OverflowException on overflow |
Design rule
- Use
checkedaround security‑critical or financial arithmetic to catch bugs early. - Use
uncheckedin hot paths where overflow is impossible or deliberately desired (e.g., hash functions).
LLM Prompt Idea
“Given my
IntegerRangeAndOverflow()method, explain the IL (addvsadd.ovf) and show the equivalent x86‑64 assembly, including how the CPU flags are used to detect overflow.”
5. Floating‑Point Precision: IEEE‑754 and Bit Patterns
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}");
}
doublefollows IEEE‑754 binary64. The decimal numbers0.1and0.2cannot be represented exactly, leading to a tiny rounding error that shows up in the sum.- The
Rformat specifier prints the round‑trip representation, guaranteeing that parsing the output yields the original binary value. BitConverter.DoubleToInt64Bitsreveals the underlying 64‑bit pattern (0x3FD999999999999Afor0.1 + 0.2).
6. Decimal: Base‑10 Arithmetic for Money
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}");
}
decimalstores 96 bits of integer data plus a scaling factor (0‑28), giving up to 28‑digit precision with exact base‑10 representation.- Ideal for financial calculations where rounding errors are unacceptable.
- The trade‑off is slower arithmetic because the CLR implements
decimaloperations in software using multiple 32‑bit integer operations.
7. Numeric Literals & Type Inference: How Suffixes Change 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()}");
}
- Suffixes (
L,f,d,m) force the compiler to emit the corresponding IL type. - Without a suffix, integer literals default to
int(orlongif the value exceedsint.MaxValue). - Floating‑point literals default to
double.
IL example (for var f = 42f;)
ldc.r4 42.0
stloc.0
Whereas 42 would compile to ldc.i4.s 42.
8. Vectorization & SIMD: Vector for Real Performance
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>abstracts hardware SIMD registers (e.g., SSE, AVX).- The JIT emits vector instructions (
addps,addpd, …) when the target CPU supports them. - Use this pattern for hot numeric loops where the data size is large enough to amortize the overhead of loading/storing vectors.
9. Using This Mental Model to Get More from LLMs
When you need the LLM to reason about performance or correctness:
- Specify the layer you care about (source → IL → JIT → CPU).
- Provide the relevant code snippet and ask for the IL or assembly.
- Ask for trade‑off analysis (e.g., “compare
floatvsdecimalfor a financial aggregation of 10 M items”). - Request micro‑benchmark ideas that isolate the JIT‑generated code.
Example prompt
“Given the
VectorizationAndSIMD()method, show the JIT‑generated assembly for the loop on an x86‑64 machine with AVX2, and explain how the CPU’s vector registers are used.”
10. Numeric‑Type Mastery Checklist (Top‑1 % Developer Mindset)
- Understand the full stack: source → Roslyn → IL → JIT → CPU.
- Know when to use
checkedvsuncheckedand the IL opcodes they generate. - Be able to read and interpret IEEE‑754 bit patterns for
float/double. - Choose
decimalfor exact base‑10 arithmetic; recognize its performance cost. - Use literal suffixes to control the emitted IL type.
- Apply
Vectoror hardware intrinsics for data‑parallel workloads. - Craft LLM prompts that reference the specific pipeline stage you’re interested in.
Armed with this knowledge, you can write numerically robust C# code and get precise, performance‑aware answers from LLMs.