콘텐츠로 이동

C# SIMD — System.Numerics & Vector 활용

SIMD(Single Instruction Multiple Data)는 하나의 명령어로 여러 데이터를 동시에 처리합니다. C#은 System.Numerics 네임스페이스를 통해 JIT가 SIMD 명령어를 자동 생성하도록 지원합니다.


1. Vector — 하드웨어 크기 자동 적응

섹션 제목: “1. Vector — 하드웨어 크기 자동 적응”
using System.Numerics;
// 현재 하드웨어의 SIMD 레지스터 크기에 맞춰 자동 조정
// SSE2: 128비트 → int 4개 / AVX2: 256비트 → int 8개
Console.WriteLine($"Vector<int> 크기: {Vector<int>.Count}"); // 4 또는 8
float[] SumArrays(float[] a, float[] b)
{
var result = new float[a.Length];
int vectorSize = Vector<float>.Count;
int i = 0;
// SIMD 처리
for (; i <= a.Length - vectorSize; i += vectorSize)
{
var va = new Vector<float>(a, i);
var vb = new Vector<float>(b, i);
(va + vb).CopyTo(result, i);
}
// 나머지 스칼라 처리
for (; i < a.Length; i++)
result[i] = a[i] + b[i];
return result;
}

2. Vector128 / Vector256 — 고정 크기 벡터

섹션 제목: “2. Vector128 / Vector256 — 고정 크기 벡터”
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
// AVX2가 지원되면 256비트 처리
void AbsoluteValue(float[] data)
{
if (Avx.IsSupported)
{
var signMask = Vector256.Create(0x7FFFFFFF).AsSingle();
int i = 0;
for (; i <= data.Length - 8; i += 8)
{
var v = Vector256.LoadUnsafe(ref data[i]);
var abs = Avx.And(v, signMask);
abs.StoreUnsafe(ref data[i]);
}
for (; i < data.Length; i++)
data[i] = MathF.Abs(data[i]);
}
else
{
for (int i = 0; i < data.Length; i++)
data[i] = MathF.Abs(data[i]);
}
}

using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
float DotProduct(float[] a, float[] b)
{
if (!Avx.IsSupported || a.Length != b.Length)
return ScalarDot(a, b);
var sum = Vector256<float>.Zero;
int i = 0;
for (; i <= a.Length - 8; i += 8)
{
var va = Vector256.LoadUnsafe(ref a[i]);
var vb = Vector256.LoadUnsafe(ref b[i]);
sum = Fma.IsSupported
? Fma.MultiplyAdd(va, vb, sum) // FMA: a*b + sum (한 명령)
: Avx.Add(Avx.Multiply(va, vb), sum);
}
// 256비트 → 스칼라 합산
var sum128 = Sse.Add(sum.GetLower(), sum.GetUpper());
sum128 = Sse.Add(sum128, Sse.MoveHighToLow(sum128, sum128));
sum128 = Sse.AddScalar(sum128, Sse.Shuffle(sum128, sum128, 1));
float result = sum128.ToScalar();
for (; i < a.Length; i++)
result += a[i] * b[i];
return result;
}

using System.Runtime.Intrinsics.Arm;
void MultiplyAdd(float[] dst, float[] src, float scalar)
{
if (AdvSimd.IsSupported)
{
var vs = Vector64.Create(scalar);
int i = 0;
for (; i <= dst.Length - 4; i += 4)
{
var vd = AdvSimd.LoadVector128(ref dst[i]);
var vsrc = AdvSimd.LoadVector128(ref src[i]);
var result = AdvSimd.FusedMultiplyAdd(vd, vsrc,
Vector128.Create(scalar));
AdvSimd.Store(ref dst[i], result);
}
for (; i < dst.Length; i++)
dst[i] += src[i] * scalar;
}
}

using System.Numerics;
int CountChar(ReadOnlySpan<byte> data, byte target)
{
int count = 0;
var vTarget = new Vector<byte>(target);
int vectorSize = Vector<byte>.Count;
int i = 0;
for (; i <= data.Length - vectorSize; i += vectorSize)
{
var chunk = new Vector<byte>(data[i..]);
var eq = Vector.Equals(chunk, vTarget);
// 일치 바이트 수 카운트
count += -Vector.Dot(eq, Vector<byte>.One);
}
for (; i < data.Length; i++)
if (data[i] == target) count++;
return count;
}

항목권장 사항
하드웨어 감지Avx2.IsSupported, AdvSimd.IsSupported 확인 후 분기
정렬가능하면 16/32바이트 정렬된 메모리 사용
루프 잔여SIMD 루프 후 스칼라 처리 코드 필수
벤치마크BenchmarkDotNet으로 실제 속도 측정
크로스 플랫폼Vector<T> 우선, Intrinsics는 폴백 포함

| Method | Mean | Speedup |
|---------------- |----------:|--------:|
| ScalarDotPro | 2,140.0 ns | 1.0x |
| Vector_DotProd | 312.5 ns | 6.8x |
| Avx2_DotProduct | 178.2 ns | 12.0x |

C# SIMD는 Vector<T>로 크로스 플랫폼 벡터화를 시작하고, 성능이 더 필요하면 Vector256과 플랫폼별 Intrinsics로 내려가는 계층적 접근이 효과적입니다. 이미지 처리, 오디오 DSP, 물리 시뮬레이션, 머신러닝 전처리 등 대용량 수치 연산에서 6~12배의 속도 향상을 기대할 수 있습니다.