Skip to content

JIT: Improve codegen for Vector128/256.NarrowWithSaturation#126226

Open
saucecontrol wants to merge 2 commits into
dotnet:mainfrom
saucecontrol:vectorconvert
Open

JIT: Improve codegen for Vector128/256.NarrowWithSaturation#126226
saucecontrol wants to merge 2 commits into
dotnet:mainfrom
saucecontrol:vectorconvert

Conversation

@saucecontrol
Copy link
Copy Markdown
Member

@saucecontrol saucecontrol commented Mar 27, 2026

Resolves #116526

This adds some missing optimized paths for NarrowWithSaturation intrinsics in pre-AVX-512 environments.

Vector128.NarrowWithSaturation was fully accelerated for signed types but not unsigned:

static Vector128<ushort> NarrowSaturate(Vector128<uint> x, Vector128<uint> y)
	=> Vector128.NarrowWithSaturation(x, y);
        vbroadcastss xmm0, dword ptr [reloc @RWD00]
        vpminuw  xmm1, xmm0, xmmword ptr [rdx]
-       vpand    xmm1, xmm1, xmm0
-       vpminuw  xmm2, xmm0, xmmword ptr [r8]
-       vpand    xmm0, xmm2, xmm0
+       vpminuw  xmm0, xmm0, xmmword ptr [r8]
        vpackuswb xmm0, xmm1, xmm0
        vmovups  xmmword ptr [rcx], xmm0
        mov      rax, rcx
        ret      

 RWD00  	dd	00FF00FFh		; 2.34184e-38
 
-; Total bytes of code 39
+; Total bytes of code 31

Vector256.NarrowWithSaturation was using the slow path for both signed and unsigned:

static Vector256<sbyte> NarrowSaturate(Vector256<short> x, Vector256<short> y)
	=> Vector256.NarrowWithSaturation(x, y);
-       vbroadcastss ymm0, dword ptr [reloc @RWD00]
-       vpmaxsw  ymm1, ymm0, ymmword ptr [rdx]
-       vbroadcastss ymm2, dword ptr [reloc @RWD04]
-       vpminsw  ymm1, ymm1, ymm2
-       vbroadcastss ymm3, dword ptr [reloc @RWD08]
-       vpand    ymm1, ymm1, ymm3
-       vpmaxsw  ymm0, ymm0, ymmword ptr [r8]
-       vpminsw  ymm0, ymm0, ymm2
-       vpand    ymm0, ymm0, ymm3
-       vpackuswb ymm0, ymm1, ymm0
+       vmovups  ymm0, ymmword ptr [rdx]
+       vpacksswb ymm0, ymm0, ymmword ptr [r8]
        vpermq   ymm0, ymm0, -40
        vmovups  ymmword ptr [rcx], ymm0
        mov      rax, rcx
        vzeroupper 
        ret      

-RWD00  	dd	FF80FF80h		;      -nan
-RWD04  	dd	007F007Fh		; 1.16633e-38
-RWD08  	dd	00FF00FFh		; 2.34184e-38
 
-; Total bytes of code 73
+; Total bytes of code 26

Full diffs

Copilot AI review requested due to automatic review settings March 27, 2026 21:04
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 27, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Mar 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 refactors x86/x64 SIMD vector conversion intrinsic selection into a shared helper and adds missing fast paths for Vector128/256.NarrowWithSaturation in non-AVX512 environments, reducing instruction count and code size for several narrow-with-saturation cases.

Changes:

  • Introduce GenTreeHWIntrinsic::GetHWIntrinsicIdForVectorConvert(...) to centralize lookup of conversion-related intrinsics (including optional saturating preference).
  • Improve Vector128/256.NarrowWithSaturation codegen on pre-AVX512 machines by using pack-based sequences where applicable.
  • Refactor existing conversion/widen/narrow construction to use the shared lookup helper instead of duplicated switch logic.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/coreclr/jit/hwintrinsicxarch.cpp Uses the new conversion lookup helper and adds optimized pack-based paths for NarrowWithSaturation on non-AVX512.
src/coreclr/jit/gentree.h Declares the new shared vector-convert intrinsic lookup helper.
src/coreclr/jit/gentree.cpp Implements the helper and refactors several SIMD convert/narrow/widen paths to use it.

Comment thread src/coreclr/jit/gentree.cpp Outdated
Comment thread src/coreclr/jit/gentree.cpp Outdated
Comment thread src/coreclr/jit/gentree.cpp Outdated
Comment thread src/coreclr/jit/gentree.cpp Outdated
Comment thread src/coreclr/jit/gentree.cpp Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/jit/hwintrinsicxarch.cpp Outdated
Comment thread src/coreclr/jit/hwintrinsicxarch.cpp
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated no new comments.

@saucecontrol
Copy link
Copy Markdown
Member Author

cc @dotnet/jit-contrib this is ready for review.

Diffs

Comment thread src/coreclr/jit/gentree.h Outdated
@JulieLeeMSFT JulieLeeMSFT added the needs-author-action An issue or pull request that requires more info or actions from the author. label May 4, 2026
@JulieLeeMSFT
Copy link
Copy Markdown
Member

@saucecontrol, please resolve comments.

@dotnet-policy-service dotnet-policy-service Bot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label May 18, 2026
Copilot AI review requested due to automatic review settings May 18, 2026 18:05
Comment thread src/coreclr/jit/hwintrinsic.cpp
Comment thread src/coreclr/jit/hwintrinsiclistxarch.h
Comment thread src/coreclr/jit/hwintrinsicxarch.cpp
@tannergooding tannergooding requested a review from EgorBo May 26, 2026 18:24
@tannergooding
Copy link
Copy Markdown
Member

The changes look correct to me, but I'm not really happy with NarrowWithSaturation going the opposite direction that we want 😅

I think the better long term thing here is to remove BaseTypeFromFirstArg, making it the default. We then only have BaseTypeFromSecondArg and can implicitly get it from the return type if no arguments exist. That helps force correctness, but also will involve a PR that finds all the intrinsics like Narrow that are incorrectly using the return type (and where it differs from the arg base type).

@saucecontrol
Copy link
Copy Markdown
Member Author

That makes sense. For this PR, I was going for consistency, because NarrowWithSaturation falls back to Narrow, and the transition of base type between the two was confusing.

I'd be happy to take on switching all of the Narrow intrinsics for xarch and aarch to BaseTypeFromFirstArg in a follow-up since that's going to be bigger. Narrow is called from quite a few places (e.g. integral vector division) where adjustments will have to be made.

Copy link
Copy Markdown
Member

@tannergooding tannergooding left a comment

Choose a reason for hiding this comment

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

CC. @dotnet/jit-contrib, @EgorBo, @kg for secondary sign-off. Community PR improving some HWIntrinsic codegen

Comment on lines +3345 to 3348
if (varTypeIsFloating(simdBaseType))
{
// gtNewSimdNarrowNode uses the base type of the return for the simdBaseType
retNode = gtNewSimdNarrowNode(retType, op1, op2, TYP_FLOAT, simdSize);
retNode = gtNewSimdNarrowNode(retType, op1, op2, simdBaseType, simdSize);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This previously always went DOUBLE -> FLOAT but now it seems like it would go DOUBLE -> DOUBLE or FLOAT -> FLOAT? I don't understand this change

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, in a nutshell, we're just delegating to the Narrow intrinsic for floating types because they have the same documented behavior, which is a saturating conversion. The Narrow intrinsic was originally modeled to have the same behavior as a C# narrowing cast, which truncates for integral types and saturates for floating. NarrowWithSaturation was added later, and just didn't need to duplicate that floating logic.

The confusion comes from the fact that Narrow is wired up so that the simdBaseType is the return type, which is the narrower type, while NarrowWithSaturation took the base type from the first arg. So the base type changed from DOUBLE to FLOAT before, only because of the differing conventions.

Per Tanner's comment (#126226 (comment)), we want to change them both to take the type from the first arg (I'll do it in a followup), but that won't change the fact that we pass through the same type when delegating to the other intrinsic. With this it goes FLOAT -> FLOAT, and later it will go DOUBLE -> DOUBLE.

}
else if ((simdSize == 16) && ((simdBaseType == TYP_SHORT) || (simdBaseType == TYP_INT)))
else if (((simdSize == 16) || (simdSize == 32)) &&
((simdBaseType == TYP_BYTE) || (simdBaseType == TYP_SHORT)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We previously would have used PackSignedSaturate here for a basetype of SHORT or INT with a size of 16; now the basetype has to be BYTE or SHORT instead of SHORT or INT. Is that intentional? Is it compensated for by the changes to the switch statement below?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, this is intentional because of the simdBaseType now matching the return type. Again, it's just for consistency with Narrow for now. We can change them both at the same time.

else if (compOpportunisticallyDependsOn(InstructionSet_AVX512))
{
if ((simdSize == 32) || (simdSize == 64))
switch (simdBaseType)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: The way the individual simd sizes (16/32/64) are being handled here is a little magical and hard to follow, a brief comment explaining the approach might be nice.

break;
}
var_types opBaseType = getHWIntrinsicWidenType(simdBaseType);
unsigned tmpSimdSize = (simdSize == 64) ? (simdSize / 2) : (simdSize * 2);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This in particular, I don't get. I assume it would make sense to me with a high level explanation comment. We're conditionally widening or narrowing based on simdSize? But this is NarrowWithSaturation, shouldn't we always be generating smaller or equal-sized vectors?

Copy link
Copy Markdown
Member Author

@saucecontrol saucecontrol May 27, 2026

Choose a reason for hiding this comment

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

I'll have to revisit this as part of the planned cleanup. I can add more comments then if that's ok with you. The basic idea is that we are narrowing two vectors into 1, so if the target isn't the max width supported by hardware, we can make the operation cheaper by building a single double-width vector and narrowing it with a single instruction. When that's not possible (because the sources are already the max supported vector width) we have to use two narrow instructions, which result in two half-size outputs that have to be rejoined. So we either need a temp size that's double or half the original size.

@kg
Copy link
Copy Markdown
Member

kg commented May 27, 2026

Will leave the checkmark to Egor for now because I can't quite make sense of what's going on in this PR (will try again later though.) I didn't see any problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Suboptimal codgen for Vector128.NarrowWithSaturation

5 participants