Parallel Data Processing
SIMD instructions process multiple values in a single CPU operation. Traditional loops handle one element per iteration. SIMD handles four, eight, or sixteen elements simultaneously using vector registers. This parallelism cuts execution time proportionally when operations fit the SIMD model.
You'll identify vectorization opportunities in real code and see baseline versus vectorized implementations. The comparison shows speedups ranging from 4x to 8x for compatible workloads. Image processing, numerical computing, and data transformation benefit most from SIMD optimization.
SIMD isn't universal. Small datasets, branch-heavy logic, and irregular memory access patterns often perform worse with vectorization. Profile your hot paths first, then apply SIMD selectively where measurements prove the benefit.
Identifying Hot Paths
Profile before vectorizing. Use BenchmarkDotNet or production profilers to find methods consuming the most CPU time. Hot paths that process arrays of numeric data are prime SIMD candidates. Look for tight loops with simple arithmetic operations and minimal branching.
Good vectorization targets include array transformations, reductions, searches, and element-wise operations. Audio signal processing, image filters, physics simulations, and data encoding all contain vectorizable patterns.
using System;
using System.Runtime.Intrinsics;
// Bad candidate: branching logic
public static int CountPositive_NotVectorizable(int[] data)
{
int count = 0;
for (int i = 0; i < data.Length; i++)
{
if (data[i] > 0) count++;
else if (data[i] < -100) count += 2;
}
return count;
}
// Good candidate: uniform operation
public static void Scale_Vectorizable(float[] data, float factor)
{
for (int i = 0; i < data.Length; i++)
{
data[i] *= factor;
}
}
// Vectorized version
public static void Scale_SIMD(float[] data, float factor)
{
if (!Vector256.IsHardwareAccelerated) { Scale_Vectorizable(data, factor); return; }
Vector256<float> vfactor = Vector256.Create(factor);
int i = 0;
for (; i <= data.Length - 8; i += 8)
{
Vector256<float> v = Vector256.LoadUnsafe(ref data[i]);
v = Vector256.Multiply(v, vfactor);
Vector256.StoreUnsafe(v, ref data[i]);
}
for (; i < data.Length; i++) data[i] *= factor;
}
Uniform operations without branches vectorize well. The SIMD version processes eight floats per iteration instead of one. Remainder elements use scalar code. This pattern delivers 6-8x speedups on large arrays.
Vector128 vs Vector256
Vector128 processes 128 bits per operation. For floats, that's four values. For ints, four values. For bytes, sixteen values. All modern CPUs support Vector128 through SSE instructions. Vector256 doubles capacity to 256 bits, processing eight floats or eight ints per operation. It requires AVX2 support, which most modern desktop CPUs provide.
Check hardware support before using vectors. IsHardwareAccelerated returns true when the CPU supports efficient vector operations. Without hardware support, vectors fall back to slow scalar emulation.
using System;
using System.Runtime.Intrinsics;
public class VectorSelection
{
public static float Sum(float[] data)
{
if (Vector256.IsHardwareAccelerated)
return Sum256(data);
else if (Vector128.IsHardwareAccelerated)
return Sum128(data);
else
return SumScalar(data);
}
private static float Sum256(float[] data)
{
Vector256<float> vsum = Vector256<float>.Zero;
int i = 0;
for (; i <= data.Length - 8; i += 8)
{
vsum = Vector256.Add(vsum, Vector256.LoadUnsafe(ref data[i]));
}
float result = Vector256.Sum(vsum);
for (; i < data.Length; i++) result += data[i];
return result;
}
private static float Sum128(float[] data)
{
Vector128<float> vsum = Vector128<float>.Zero;
int i = 0;
for (; i <= data.Length - 4; i += 4)
{
vsum = Vector128.Add(vsum, Vector128.LoadUnsafe(ref data[i]));
}
float result = Vector128.Sum(vsum);
for (; i < data.Length; i++) result += data[i];
return result;
}
private static float SumScalar(float[] data)
{
float sum = 0;
for (int i = 0; i < data.Length; i++) sum += data[i];
return sum;
}
}
This pattern provides optimal code for all platforms. Modern CPUs use Vector256 for 8-wide operations. Older hardware uses Vector128 for 4-wide operations. Ancient or embedded CPUs fall back to scalar code. All paths produce correct results.
Memory Alignment and Loading
LoadUnsafe works with any array regardless of alignment. It's safe and performs well on modern CPUs. Older documentation mentions aligned loads for performance, but current hardware handles unaligned loads efficiently. Use LoadUnsafe unless you have specific alignment guarantees and profiling shows aligned loads help.
StoreUnsafe writes vector data back to arrays. Like LoadUnsafe, it handles unaligned data safely. The unsafe name indicates it skips bounds checks, not that it's actually unsafe with proper usage.
using System;
using System.Runtime.Intrinsics;
public class VectorLoading
{
public static void Negate(int[] data)
{
int i = 0;
for (; i <= data.Length - 4; i += 4)
{
Vector128<int> v = Vector128.LoadUnsafe(ref data[i]);
v = Vector128.Negate(v);
Vector128.StoreUnsafe(v, ref data[i]);
}
for (; i < data.Length; i++) data[i] = -data[i];
}
public static void Add(int[] a, int[] b, int[] result)
{
int i = 0;
for (; i <= a.Length - 4; i += 4)
{
Vector128<int> va = Vector128.LoadUnsafe(ref a[i]);
Vector128<int> vb = Vector128.LoadUnsafe(ref b[i]);
Vector128<int> vr = Vector128.Add(va, vb);
Vector128.StoreUnsafe(vr, ref result[i]);
}
for (; i < a.Length; i++) result[i] = a[i] + b[i];
}
}
Load and store operations access memory without copying. They read or write vector-sized chunks directly. Multiple loads let you process multiple arrays simultaneously, as shown in the Add example.
Performance Notes
SIMD speedups range from 4x to 8x for vectorizable workloads. Image processing sees 6-8x gains when applying filters or color transforms. Audio processing benefits similarly when mixing streams or applying effects. Physics simulations accelerate when updating particle arrays in bulk.
Small arrays show minimal gains. Vector setup overhead dominates when processing under 100 elements. Large arrays of thousands of elements show full speedup potential. Benchmark with realistic dataset sizes to verify benefits match your use case.
Memory bandwidth can limit gains. If your operation is memory-bound rather than compute-bound, SIMD won't help much. Profile to identify bottlenecks. SIMD works best when computation cost exceeds memory access cost.
Benchmark Playground
Steps
- dotnet new console -n SimdHotPath
- cd SimdHotPath
- Update Program.cs and .csproj
- dotnet run -c Release
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Runtime.Intrinsics;
BenchmarkRunner.Run<VectorBench>();
[MemoryDiagnoser]
public class VectorBench
{
private float[] _data;
[Params(1000, 10000)]
public int Size { get; set; }
[GlobalSetup]
public void Setup() => _data = new float[Size];
[Benchmark(Baseline = true)]
public void Scalar()
{
for (int i = 0; i < _data.Length; i++)
_data[i] *= 2.0f;
}
[Benchmark]
public void Vector()
{
Vector256<float> factor = Vector256.Create(2.0f);
int i = 0;
for (; i <= _data.Length - 8; i += 8)
{
Vector256<float> v = Vector256.LoadUnsafe(ref _data[i]);
v = Vector256.Multiply(v, factor);
Vector256.StoreUnsafe(v, ref _data[i]);
}
for (; i < _data.Length; i++) _data[i] *= 2.0f;
}
}
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup><ItemGroup><PackageReference Include="BenchmarkDotNet" Version="0.13.12" /></ItemGroup></Project>
What you'll see
Vector version runs 6-8x faster than scalar for 10000 elements. At 1000 elements, speedup drops to 4-5x due to setup overhead relative to total work.
Scalability Notes
SIMD delivers measurable gains in numeric processing hot paths. Image filters, audio effects, physics updates, and batch transformations all benefit when operations are uniform and datasets are large. Production metrics show throughput increases of 4-8x in vectorized code paths.
Identify hot paths through profiling. Use BenchmarkDotNet to compare scalar versus SIMD implementations with realistic data sizes. If speedups exceed 3x and the code path consumes significant CPU time, vectorization pays off.
Test on target hardware. Vector performance varies across CPUs. Older machines might not support Vector256. Some embedded processors lack SIMD entirely. Always provide scalar fallbacks and test on representative hardware.
Balance complexity against gains. SIMD code is harder to read and maintain. If profiling shows modest improvements or hot paths consume minimal CPU time, scalar code keeps your codebase clearer. Apply SIMD selectively where measurements prove significant benefits.