Skip to content

Normalize SIMD pipelines using TensorPrimitives_ - #3161

Merged
JimBobSquarePants merged 21 commits into
mainfrom
perf/simd-primitives-normalization
Jul 26, 2026
Merged

Normalize SIMD pipelines using TensorPrimitives_#3161
JimBobSquarePants merged 21 commits into
mainfrom
perf/simd-primitives-normalization

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented Jul 25, 2026

Copy link
Copy Markdown
Member

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 👮.
  • 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

OperationElementsBeforeAfterRatioChange
Clampprocessor workload45.09 ns38.47 ns0.8614.7% faster
ICC max25629.52 ns13.26 ns0.4555.1% faster
ICC max2,048230.41 ns96.90 ns0.4257.9% faster
ICC multiply25610.023 ns6.549 ns0.6534.7% faster
ICC multiply2,04882.854 ns63.982 ns0.7722.8% faster
JPEG multiply2569.853 ns7.835 ns0.8020.5% faster
JPEG multiply2,04864.993 ns45.332 ns0.7030.3% faster
Single add25612.31 ns10.09 ns0.8218.0% faster
Single add2,048102.45 ns58.72 ns0.5742.7% faster
UInt32 add25611.430 ns9.954 ns0.8712.9% faster
UInt32 add2,048103.460 ns73.943 ns0.7128.5% faster
Byte add641.637 ns2.165 ns1.3232.3% slower
Byte add2563.644 ns3.727 ns1.022.3% slower
Byte add2,04824.865 ns24.601 ns0.991.1% faster
Byte add8,192122.624 ns103.183 ns0.8415.9% faster
Resize kernel normalization72.807 ns2.827 ns1.010.7% slower
Resize kernel normalization193.147 ns5.003 ns1.5959.0% slower
Resize kernel normalization323.066 ns3.603 ns1.1817.5% slower
Resize kernel normalization653.818 ns5.327 ns1.4039.5% slower
Resize kernel normalization1619.283 ns7.869 ns0.8515.2% faster
Resize kernel normalization25715.341 ns9.234 ns0.6039.8% faster
Resize kernel normalization2,048125.141 ns63.633 ns0.5149.2% faster

JPEG packing pipelines

PipelineElementsBeforeAfterRatioChangeManaged allocation
Invert + normalize + interleave 4128103.39 ns50.34 ns0.4951.3% faster0 B / 0 B
Invert + normalize + interleave 41,024862.64 ns343.56 ns0.4060.2% faster0 B / 0 B
Invert + normalize + interleave 44,0963,561.36 ns1,313.44 ns0.3763.1% faster0 B / 0 B
Normalize + interleave 312878.25 ns43.30 ns0.5644.7% faster0 B / 0 B
Normalize + interleave 31,024603.88 ns307.28 ns0.5149.1% faster0 B / 0 B
Normalize + interleave 34,0962,332.94 ns1,219.93 ns0.5347.7% faster0 B / 0 B
Normalize + interleave 412898.87 ns39.52 ns0.4060.0% faster0 B / 0 B
Normalize + interleave 41,024716.14 ns283.87 ns0.4060.4% faster0 B / 0 B
Normalize + interleave 44,0962,959.24 ns1,107.54 ns0.3862.6% faster0 B / 0 B
Deinterleave 312880.09 ns40.41 ns0.5149.5% faster0 B / 0 B
Deinterleave 31,024592.62 ns292.62 ns0.5050.6% faster0 B / 0 B
Deinterleave 34,0962,304.31 ns1,128.42 ns0.4951.0% faster0 B / 0 B

Additional migrations found during the library-wide audit

OperationElementsBeforeAfterRatioChangeManaged allocation
Histogram scalar add25697.631 ns8.464 ns0.0991.3% faster (11.54x)0 B / 0 B
Histogram scalar add65,53624.430 us1.640 us0.0793.3% faster (14.90x)0 B / 0 B
Sharpen-kernel negate73.901 ns2.489 ns0.6436.2% faster0 B / 0 B
Sharpen-kernel negate199.106 ns2.895 ns0.3268.2% faster0 B / 0 B
Sharpen-kernel negate6529.919 ns4.502 ns0.1585.0% faster0 B / 0 B
Sharpen-kernel negate16183.357 ns4.721 ns0.0694.3% faster0 B / 0 B
Gaussian normalization73.677 ns2.898 ns0.7921.2% faster0 B / 0 B
Gaussian normalization1910.079 ns5.222 ns0.5248.2% faster0 B / 0 B
Gaussian normalization6532.780 ns5.239 ns0.1684.0% faster0 B / 0 B
Gaussian normalization16188.170 ns7.029 ns0.0892.0% faster0 B / 0 B
Bokeh complex-kernel scale1711.286 ns8.420 ns0.7525.4% faster0 B / 0 B
Bokeh complex-kernel scale3321.040 ns3.732 ns0.1882.3% faster0 B / 0 B
Bokeh complex-kernel scale6541.041 ns4.577 ns0.1188.8% faster0 B / 0 B
Bokeh complex-kernel scale161110.389 ns9.590 ns0.0991.3% faster0 B / 0 B
ICC byte LUT normalization256145.188 ns9.997 ns0.0793.1% faster (14.52x)0 B / 0 B
ICC UInt16 LUT normalization256143.157 ns11.771 ns0.0891.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.

PipelinePayload bytesBeforeAfterRatioChangeManaged allocation
PNG Up-filter decode2563.939 ns6.779 ns1.7272.1% slower0 B / 0 B
PNG Up-filter decode2,04820.861 ns18.692 ns0.9010.4% faster0 B / 0 B
PNG Up-filter decode8,19293.117 ns65.930 ns0.7129.2% faster0 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.

ArtifactmainThis PRReduction
Managed assembly (SixLabors.ImageSharp.dll)4,100,608 B2,433,024 B1,667,584 B (40.67%)
Portable symbols (SixLabors.ImageSharp.pdb)2,033,448 B1,047,496 B985,952 B (48.49%)
Packed NuGet package1,316,135 B1,278,182 B37,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

VersionNative executableChange
main9,475,584 B
This PR9,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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR normalizes several SIMD-heavy pipelines around a new internal TensorPrimitives_ compatibility API (mirroring System.Numerics.Tensors.TensorPrimitives) and removes older, parallel SIMD implementations in favor of a single runtime-shaped dispatch model. It also adds targeted tests and benchmarks to validate correctness across scalar/SIMD width boundaries and to track performance impacts.

Changes:

  • Introduce and adopt TensorPrimitives_-style span operations for common add/multiply/divide/negate/max workloads across codecs and processing helpers.
  • Refactor/normalize affine Vector4 conversion traversal and pixel blender traversal using operator-based SIMD dispatch, and remove legacy JPEG converter SIMD class hierarchy.
  • Add tests/benchmarks covering SIMD boundary behavior for PNG filters, ICC LUT normalization, pixel blending, convolution kernels, and Vector4 affine transforms.

Reviewed changes

Copilot reviewed 109 out of 111 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.csAdds regression test for Gaussian sharpen kernel correctness across sizes.
tests/ImageSharp.Tests/PixelFormats/Vector4ConvertersTests.csNew tests validating affine Vector4 conversion traversal across hardware-width boundaries.
tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.csStrengthens blender tests to cover multiple bulk overloads, widths, and coverage/amount variants.
tests/ImageSharp.Tests/Metadata/Profiles/ICC/Various/IccLutTests.csNew tests asserting bit-exact ICC LUT normalization for byte/ushort inputs (including exhaustive ushort).
tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.csAdds encoder/reference comparisons across SIMD boundaries and pixel strides; minor formatting tidy.
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csprojAdds conditional net10 dependency on System.Numerics.Tensors for benchmarks.
tests/ImageSharp.Benchmarks/General/PixelConversion/Vector4AffineTransform.csNew benchmark for operator-driven affine Vector4 transforms.
tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.csNew benchmark for shared PNG filter encode traversal.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YccKColorConverter.csUpdates benchmark to use adaptive converter selection rather than per-width implementations.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/YCbCrColorConversion.csUpdates benchmark to use adaptive converter selection rather than per-width implementations.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/RgbColorConversion.csUpdates benchmark to use adaptive converter selection rather than per-width implementations.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPackingScalar.csNew preserved scalar JPEG packing loops for before/after comparison.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/JpegColorPacking.csNew benchmark comparing scalar packing vs SIMD transpose implementations.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/GrayscaleColorConversion.csUpdates benchmark to use adaptive converter selection rather than per-width implementations.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/ColorConversionBenchmark.csFixes benchmark setup to actually populate allocated buffers.
tests/ImageSharp.Benchmarks/Codecs/Jpeg/ColorConversion/CmykColorConversion.csUpdates benchmark to use adaptive converter selection rather than per-width implementations.
tests/ImageSharp.Benchmarks/Bulk/ShuffleByte4Channel.csUpdates shuffle benchmark call to match new shuffle API shape.
tests/ImageSharp.Benchmarks/Bulk/Shuffle4Slice3Channel.csUpdates shuffle benchmark to generic shuffle operators, removing control instance.
tests/ImageSharp.Benchmarks/Bulk/Shuffle3Channel.csUpdates shuffle benchmark to generic shuffle operators, removing control instance.
tests/ImageSharp.Benchmarks/Bulk/Pad3Shuffle4Channel.csUpdates pad+shuffle benchmark to generic shuffle operators, removing control instance.
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.csReplaces scalar histogram add loop with TensorPrimitives_ add.
src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.csNormalizes complex-kernel scaling via TensorPrimitives_ over a flattened float span.
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.csReplaces per-element kernel normalization/inversion loops with TensorPrimitives_ operations.
src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.csAdds stateful operator definitions for affine Vector4 transforms across register widths.
src/ImageSharp/PixelFormats/Utils/Vector4Converters.Affine.csRefactors affine traversal into a single generic operator-driven Apply loop and makes APIs public.
src/ImageSharp/PixelFormats/PixelBlenders/IPixelBlenderOperator.csNew static-abstract operator contract for shared blender traversal across SIMD widths.
src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlender{TPixel,TOperator}.csRefactors associated-alpha blender to use the new operator-based blender base and clamps amount.
src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.csRoutes LUT normalization through a dedicated normalizer helper.
src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.csReplaces AVX2-only add loop with TensorPrimitives_ add over sliced spans and removes old helper.
src/ImageSharp/Formats/Webp/AlphaDecoder.csReplaces SIMD/scalar branches with TensorPrimitives_ byte add for vertical unfiltering.
src/ImageSharp/Formats/Png/Filters/SubFilter.csReplaces bespoke encode loop with shared PngFilterEncoder traversal.
src/ImageSharp/Formats/Png/Filters/AverageFilter.csReplaces bespoke encode loop with shared PngFilterEncoder traversal.
src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.csReplaces manual SIMD/scalar sum/multiply loops with TensorPrimitives_ add/multiply.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector512.csRemoves legacy Vector512 converter base class.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector256.csRemoves legacy Vector256 converter base class.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector128.csRemoves legacy Vector128 converter base class.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterVector.csRemoves legacy System.Numerics.Vector converter base class.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverterScalar.csRemoves legacy scalar converter base class.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector512.csRemoves legacy YccK Vector512 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector256.csRemoves legacy YccK Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKVector128.csRemoves legacy YccK Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.csRemoves legacy YccK scalar implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector512.csRemoves legacy YCbCr Vector512 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector256.csRemoves legacy YCbCr Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrVector128.csRemoves legacy YCbCr Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.csRemoves legacy YCbCr scalar implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector256.csRemoves legacy TIFF YccK Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKVector128.csRemoves legacy TIFF YccK Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.csRemoves legacy TIFF YccK scalar implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector512.csRemoves legacy TIFF CMYK Vector512 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector256.csRemoves legacy TIFF CMYK Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykVector128.csRemoves legacy TIFF CMYK Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffCmykScalar.csRemoves legacy TIFF CMYK scalar implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector512.csRemoves legacy RGB Vector512 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector256.csRemoves legacy RGB Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbVector128.csRemoves legacy RGB Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbScalar.csRemoves legacy RGB scalar implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.RgbOperator.csAdds operator-based RGB converter implementation (scalar + SIMD lanes + ICC path).
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector512.csRemoves legacy grayscale Vector512 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector256.csRemoves legacy grayscale Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleVector128.csRemoves legacy grayscale Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.GrayScaleScalar.csRemoves legacy grayscale scalar implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector512.csRemoves legacy CMYK Vector512 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector256.csRemoves legacy CMYK Vector256 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykVector128.csRemoves legacy CMYK Vector128 implementation.
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.CmykScalar.csRemoves legacy CMYK scalar implementation.
src/ImageSharp/Common/Helpers/Vector512Utilities.csAdds portable Vector512<byte> subtract-saturate helper mirroring runtime behavior.
src/ImageSharp/Common/Helpers/Vector256Utilities.csAligns subtract-saturate fallback behavior/comments with runtime portable approach.
src/ImageSharp/Common/Helpers/Vector128Utilities.csSimplifies unsigned byte subtract-saturate fallback to a min/sub identity.
src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.csAdds generic operator-based multiply traversal (scalar + 128/256/512).
src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.csAdds divide traversal with restricted vectorization rules to match runtime capabilities.
src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.csAdds generic operator-based add traversal (span+span and span+scalar).
src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.csReplaces stateful shuffle control with stateless operator (ZYXShuffle3) + intrinsic helper.
src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.csRefactors shuffle contract to static-abstract scalar/vector operators.
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.csReplaces ad-hoc SIMD/scalar loops with TensorPrimitives_ max/multiply over flattened Vector4 spans.

Comment threadtests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs
CopilotAI review requested due to automatic review settings July 25, 2026 23:57

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 109 out of 111 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

tests/ImageSharp.Benchmarks/Codecs/Png/PngFilterEncode.cs:87

  • AverageFilter.Encode expects uint bytesPerPixel, but this benchmark passes BytesPerPixel as an int, which will not compile once the Encode signature is updated. Cast the constant to uint to match the API (consistent with other call sites in the PR).

@JimBobSquarePantsJimBobSquarePants added this to the v4.1.0 milestone Jul 26, 2026
CopilotAI review requested due to automatic review settings July 26, 2026 08:53
This was referenced Aug 17, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@JimBobSquarePants