-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Add IsKnownConstant jit helper and optimize 'str == ""' with str.StartsWith('c') #63734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT Issue DetailsIntroduce an internal API Unfortunately, Example:bool Test(string s) => s == "";Current codegen; Method Program:Test1(System.String):bool
sub rsp, 40
mov r8, 0xD1FFAB1E ; ""
mov rdx, gword ptr [r8]
cmp rcx, rdx
jne SHORT G_M22776_IG04
mov eax, 1
jmp SHORT G_M22776_IG07
G_M22776_IG04:
test rcx, rcx
je SHORT G_M22776_IG05
mov r8d, dword ptr [rcx+8]
test r8d, r8d
je SHORT G_M22776_IG06
G_M22776_IG05:
xor eax, eax
jmp SHORT G_M22776_IG07
G_M22776_IG06:
add rcx, 12
add rdx, 12
add r8d, r8d
call System.SpanHelpers:SequenceEqual(byref,byref,long):bool
G_M22776_IG07:
nop
add rsp, 40
ret
; Total bytes of code: 69
; Method Program:Test2(System.String):bool
cmp dword ptr [rcx+8], 0
je SHORT G_M8187_IG04
cmp word ptr [rcx+12], 115
sete al
movzx rax, al
jmp SHORT G_M8187_IG05
G_M8187_IG04:
xor eax, eax
G_M8187_IG05:
ret
; Total bytes of code: 22New codegen:; Method Program:Test1(System.String):bool
test rcx, rcx
jne SHORT G_M22776_IG04
xor eax, eax
jmp SHORT G_M22776_IG05
G_M22776_IG04:
cmp dword ptr [rcx+8], 0
sete al
movzx rax, al
ret
; Total bytes of code: 20
; Method Program:Test2(System.String):bool
cmp word ptr [rcx+12], 115
sete al
movzx rax, al
ret
; Total bytes of code: 12TODO: Mono-Interp, Mono and Mono-LLVM. Closes #11484 Not sure about "generic" signature here, it might e.g. brake inlining due to runtime-lookup. Perhaps, it's better to introduce overloads for all primitives? Thoughts? cc @stephentoub @GrabYourPitchforks @jkotas @dotnet/jit-contrib
|
src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs
Outdated
Show resolved
Hide resolved
…son.cs Co-authored-by: Miha Zupan <[email protected]>
|
This API helps to declare optimizations directly in C# code, because |
src/coreclr/jit/importer.cpp
Outdated
|
|
||
| case NI_System_Runtime_CompilerServices_RuntimeHelpers_IsKnownConstant: | ||
| { | ||
| if (opts.OptimizationEnabled()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to check OptimizationEnabled here? We have have early out for disabled optimizations before this switch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unnecessary OptimizationEnabled check is in a few other intrinsics as well. You can delete it there too while you are on it.
src/coreclr/jit/importer.cpp
Outdated
| if (opts.OptimizationEnabled()) | ||
| { | ||
| GenTree* op1 = impPopStack().val; | ||
| if (op1->OperIsConst()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String literals get fetched via extra indirections. How does this work? Where do the extra indirections get the GTK_CONST flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, the PR description mentions that this does not work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string.Empty and static readonly string don't work (but can be arranged). Normal string literals do work because we import them as GT_CNS_STR and expand to indirections later in morph
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly for diffs, we prefer string.Empty over "" in BCL, also all comparisons against empty strings were manually converted to str?.Length == 0 over the codebase in https://github.com/dotnet/runtime/pull/36443/files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also all comparisons against empty strings were manually converted to str?.Length == 0 over the codebase in https://github.com/dotnet/runtime/pull/36443/files
To be clear, they weren't converted to str?.Length == 0, or at least not 99% of them (I just re-skimmed the diff and didn't see any of those). These were all cases where we'd already checked the string for null, and so a simple length check was sufficient and clear, or where the string was being checked for null or empty, and thus it was clearer and more consistent with guidelines to use IsNullOrEmpty. I'd have pushed back on a change that tried to use str?.Length == 0 everywhere :)
(That said, it speaks to just how common == "" actually is in code.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub ah makes sense for me now why your analyzer found 100 cases in #63719
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should CA1820 be removed when all these changes defeat its perf improvement reasoning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should CA1820 be removed when all these changes defeat its perf improvement reasoning?
It's already disabled by default. If there ends up being zero benefit after these improvements (including subsequent ones to get string.Empty and the like), we could consider deleting it entirely from the SDK. I wouldn't rush to that, though.
Co-authored-by: SingleAccretion <[email protected]>
Co-authored-by: SingleAccretion <[email protected]>
void Foo(int a, ...)
{
var _ = RuntimeHelpers.IsKnownConstant(a);
// a lot of code
}A hack to force inliner to inline |
src/coreclr/jit/inlinepolicy.cpp
Outdated
|
|
||
| if (m_ArgFeedsIsKnownConst) | ||
| { | ||
| // In IsPrejitRoot we don't have callsite info so let's assume we have a constant here in order to avoid "baked" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my education, what is the callsite info that is missing for IsPrejitRoot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jkotas It's a separate run during prejitting where we just take every function (without context) and try to memorize all those which will never be inlined (by reporting to VM "mark this function as noinline" here). During that, we opportunistically assume the context is the most inliner friendly: all imaginary arguments are constants, imaginary callsite has PGO data and is hot, etc.. there was a better/detailed explanation from Andy somewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we leverage prejitting to mark methods that will never be inlined so we don't spend any jit time analyzing them.
|
Jit-diff (-f --crossgen): All regressions are block layout-related, can be fixed with it's ready for review |
Maybe we need an [AggressivelyConsiderInlinining] :) |
|
@dotnet/jit-contrib PTAL |
| if ((cnsIndex < length) && (str != nullptr)) | ||
| { | ||
| GenTree* cnsCharNode = gtNewIconNode(str[cnsIndex], elemTyp); | ||
| GenTree* cnsCharNode = gtNewIconNode(str[cnsIndex], TYP_INT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated change: "str"[0] used to be folded into a CNS_INT with TYP_USHORT type - no idea how it didn't assert anywhere
jakobbotsch
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Are we worried about the extra IL in these heavily used functions? Do we know of other tools/compilers/interpreters that might get tripped up?
Good question - the function always return false by default so Mono should be able to fold these branches and be not impacted (both mini and LLVM). Unfortunately, I wasn't able to properly implement this intrinsic for Mono because it doesn't propagate constants during inlining. I'll check Mono-Interp but since it's also already pretty advanced and is able to fold branches before actual interpretation I assume it's also not impacted anyhow by this change. |
internal class Program
{
static void Main() => Test("");
[MethodImpl(MethodImplOptions.NoInlining)]
static bool Test(string s) => s == "";
}Diffs for |

Introduce an internal API
bool RuntimeHelpers.IsKnownConstant<T>(T t)(see #11484) and optimizeString.Equalswith it to simplify codegen for comparisons against const strings. Same forString.StartsWith(char c). Also, JIT's inliner was improved to recognize it.Unfortunately,
string.Emptyis not yet recognized as a constant, it needs #44847Example:
Current codegen
New codegen:
NOTE: no diffs for these functions when args aren't constants
Can be also implemented in Mono (e.g. for Mono-LLVM: https://llvm.org/docs/LangRef.html#llvm-is-constant-intrinsic)
Closes #11484
Closes #63719
Closes #41779
Not sure about "generic" signature here, it might e.g. brake inlining due to runtime-lookup. Perhaps, it's better to introduce overloads for all primitives (or add overloads on demand)? Thoughts? cc @stephentoub @GrabYourPitchforks @jkotas @dotnet/jit-contrib