Normalize SIMD pipelines using TensorPrimitives_
Prerequisites
- I have written a descriptive pull-request title
- I have verified that there are no overlapping pull-requests open
- I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules :cop:.
- I have provided test coverage for my change (where applicable)
Description
This change introduces the internal TensorPrimitives_ compatibility type and uses the runtime's generic
operator-based SIMD model to normalize ImageSharp's span operations. The compatibility API follows
System.Numerics.Tensors.TensorPrimitives so it can be replaced by the runtime implementation when ImageSharp
moves to .NET 10.
The migration removes the replaced legacy SIMD implementations rather than retaining parallel pipelines. It also
uses portable Vector128, Vector256, and Vector512 APIs where the target framework exposes them, with
platform-specific intrinsics isolated behind compatibility helpers only where .NET 8 has no equivalent API.
The normalized implementation follows runtime main's widest-supported dispatch for every supported element type:
AVX-512 is selected when one complete Vector512<T> fits, and shorter inputs fall through to a width that fits.
SIMD layouts, masks, shuffles, overlap handling, scalar equivalence, and width-selection decisions are documented inline.
Performance
The tables below compare the implementation removed by this PR ("Before") with the retained normalized
implementation ("After"). Lower is better.
MemoryDiagnoser was enabled for every row containing an allocation result. 0 B / 0 B means BenchmarkDotNet
measured zero managed bytes allocated per operation for both the before and after implementations.
Tensor primitive migrations
| Operation | Elements | Before | After | Ratio | Change |
|---|---|---|---|---|---|
| Clamp | processor workload | 45.09 ns | 38.47 ns | 0.86 | 14.7% faster |
| ICC max | 256 | 29.52 ns | 13.26 ns | 0.45 | 55.1% faster |
| ICC max | 2,048 | 230.41 ns | 96.90 ns | 0.42 | 57.9% faster |
| ICC multiply | 256 | 10.023 ns | 6.549 ns | 0.65 | 34.7% faster |
| ICC multiply | 2,048 | 82.854 ns | 63.982 ns | 0.77 | 22.8% faster |
| JPEG multiply | 256 | 9.853 ns | 7.835 ns | 0.80 | 20.5% faster |
| JPEG multiply | 2,048 | 64.993 ns | 45.332 ns | 0.70 | 30.3% faster |
| Single add | 256 | 12.31 ns | 10.09 ns | 0.82 | 18.0% faster |
| Single add | 2,048 | 102.45 ns | 58.72 ns | 0.57 | 42.7% faster |
| UInt32 add | 256 | 11.430 ns | 9.954 ns | 0.87 | 12.9% faster |
| UInt32 add | 2,048 | 103.460 ns | 73.943 ns | 0.71 | 28.5% faster |
| Byte add | 64 | 1.637 ns | 2.165 ns | 1.32 | 32.3% slower |
| Byte add | 256 | 3.644 ns | 3.727 ns | 1.02 | 2.3% slower |
| Byte add | 2,048 | 24.865 ns | 24.601 ns | 0.99 | 1.1% faster |
| Byte add | 8,192 | 122.624 ns | 103.183 ns | 0.84 | 15.9% faster |
| Resize kernel normalization | 7 | 2.807 ns | 2.827 ns | 1.01 | 0.7% slower |
| Resize kernel normalization | 19 | 3.147 ns | 5.003 ns | 1.59 | 59.0% slower |
| Resize kernel normalization | 32 | 3.066 ns | 3.603 ns | 1.18 | 17.5% slower |
| Resize kernel normalization | 65 | 3.818 ns | 5.327 ns | 1.40 | 39.5% slower |
| Resize kernel normalization | 161 | 9.283 ns | 7.869 ns | 0.85 | 15.2% faster |
| Resize kernel normalization | 257 | 15.341 ns | 9.234 ns | 0.60 | 39.8% faster |
| Resize kernel normalization | 2,048 | 125.141 ns | 63.633 ns | 0.51 | 49.2% faster |
JPEG packing pipelines
| Pipeline | Elements | Before | After | Ratio | Change | Managed allocation |
|---|---|---|---|---|---|---|
| Invert + normalize + interleave 4 | 128 | 103.39 ns | 50.34 ns | 0.49 | 51.3% faster | 0 B / 0 B |
| Invert + normalize + interleave 4 | 1,024 | 862.64 ns | 343.56 ns | 0.40 | 60.2% faster | 0 B / 0 B |
| Invert + normalize + interleave 4 | 4,096 | 3,561.36 ns | 1,313.44 ns | 0.37 | 63.1% faster | 0 B / 0 B |
| Normalize + interleave 3 | 128 | 78.25 ns | 43.30 ns | 0.56 | 44.7% faster | 0 B / 0 B |
| Normalize + interleave 3 | 1,024 | 603.88 ns | 307.28 ns | 0.51 | 49.1% faster | 0 B / 0 B |
| Normalize + interleave 3 | 4,096 | 2,332.94 ns | 1,219.93 ns | 0.53 | 47.7% faster | 0 B / 0 B |
| Normalize + interleave 4 | 128 | 98.87 ns | 39.52 ns | 0.40 | 60.0% faster | 0 B / 0 B |
| Normalize + interleave 4 | 1,024 | 716.14 ns | 283.87 ns | 0.40 | 60.4% faster | 0 B / 0 B |
| Normalize + interleave 4 | 4,096 | 2,959.24 ns | 1,107.54 ns | 0.38 | 62.6% faster | 0 B / 0 B |
| Deinterleave 3 | 128 | 80.09 ns | 40.41 ns | 0.51 | 49.5% faster | 0 B / 0 B |
| Deinterleave 3 | 1,024 | 592.62 ns | 292.62 ns | 0.50 | 50.6% faster | 0 B / 0 B |
| Deinterleave 3 | 4,096 | 2,304.31 ns | 1,128.42 ns | 0.49 | 51.0% faster | 0 B / 0 B |
Additional migrations found during the library-wide audit
| Operation | Elements | Before | After | Ratio | Change | Managed allocation |
|---|---|---|---|---|---|---|
| Histogram scalar add | 256 | 97.631 ns | 8.464 ns | 0.09 | 91.3% faster (11.54x) | 0 B / 0 B |
| Histogram scalar add | 65,536 | 24.430 us | 1.640 us | 0.07 | 93.3% faster (14.90x) | 0 B / 0 B |
| Sharpen-kernel negate | 7 | 3.901 ns | 2.489 ns | 0.64 | 36.2% faster | 0 B / 0 B |
| Sharpen-kernel negate | 19 | 9.106 ns | 2.895 ns | 0.32 | 68.2% faster | 0 B / 0 B |
| Sharpen-kernel negate | 65 | 29.919 ns | 4.502 ns | 0.15 | 85.0% faster | 0 B / 0 B |
| Sharpen-kernel negate | 161 | 83.357 ns | 4.721 ns | 0.06 | 94.3% faster | 0 B / 0 B |
| Gaussian normalization | 7 | 3.677 ns | 2.898 ns | 0.79 | 21.2% faster | 0 B / 0 B |
| Gaussian normalization | 19 | 10.079 ns | 5.222 ns | 0.52 | 48.2% faster | 0 B / 0 B |
| Gaussian normalization | 65 | 32.780 ns | 5.239 ns | 0.16 | 84.0% faster | 0 B / 0 B |
| Gaussian normalization | 161 | 88.170 ns | 7.029 ns | 0.08 | 92.0% faster | 0 B / 0 B |
| Bokeh complex-kernel scale | 17 | 11.286 ns | 8.420 ns | 0.75 | 25.4% faster | 0 B / 0 B |
| Bokeh complex-kernel scale | 33 | 21.040 ns | 3.732 ns | 0.18 | 82.3% faster | 0 B / 0 B |
| Bokeh complex-kernel scale | 65 | 41.041 ns | 4.577 ns | 0.11 | 88.8% faster | 0 B / 0 B |
| Bokeh complex-kernel scale | 161 | 110.389 ns | 9.590 ns | 0.09 | 91.3% faster | 0 B / 0 B |
| ICC byte LUT normalization | 256 | 145.188 ns | 9.997 ns | 0.07 | 93.1% faster (14.52x) | 0 B / 0 B |
| ICC UInt16 LUT normalization | 256 | 143.157 ns | 11.771 ns | 0.08 | 91.8% faster (12.16x) | 0 B / 0 B |
Byte-add production callers
The PNG rows compare the exact AVX2 loop removed from UpFilter.Decode with the production
TensorPrimitives_.Add<byte> call in the same benchmark run. The runtime-shaped implementation has a
small-buffer dispatch cost, crosses over by 2,048 payload bytes, and benefits increasingly from AVX-512 on
larger scanlines.
| Pipeline | Payload bytes | Before | After | Ratio | Change | Managed allocation |
|---|---|---|---|---|---|---|
| PNG Up-filter decode | 256 | 3.939 ns | 6.779 ns | 1.72 | 72.1% slower | 0 B / 0 B |
| PNG Up-filter decode | 2,048 | 20.861 ns | 18.692 ns | 0.90 | 10.4% faster | 0 B / 0 B |
| PNG Up-filter decode | 8,192 | 93.117 ns | 65.930 ns | 0.71 | 29.2% faster | 0 B / 0 B |
Binary size
Release net8.0 artifacts were built from main and this PR using the same .NET SDK 10.0.302 installation and
project settings.
| Artifact | main | This PR | Reduction |
|---|---|---|---|
Managed assembly (SixLabors.ImageSharp.dll) | 4,100,608 B | 2,433,024 B | 1,667,584 B (40.67%) |
Portable symbols (SixLabors.ImageSharp.pdb) | 2,033,448 B | 1,047,496 B | 985,952 B (48.49%) |
| Packed NuGet package | 1,316,135 B | 1,278,182 B | 37,953 B (2.88%) |
The package reduction is smaller than the assembly reduction because the removed generated IL is highly
repetitive and therefore compresses efficiently inside the NuGet package.
NativeAOT binary-size impact
| Version | Native executable | Change |
|---|---|---|
main | 9,475,584 B | — |
| This PR | 9,652,736 B | +177,152 B (1.87%) |
Sizoscope attributes the remaining increase predominantly to statically specialized SIMD/operator code. Further material sharing would require runtime dispatch, reduced inlining, or eager initialization, potentially harming performance.
Both executables completed the representative processing and codec workload successfully with identical output.