Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix offset + globalization issues in StringSegment - #45022

Merged
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment
Jan 23, 2021
Merged

Fix offset + globalization issues in StringSegment#45022
GrabYourPitchforks merged 7 commits into
dotnet:masterfrom
GrabYourPitchforks:stringsegment

Conversation

@GrabYourPitchforks

Copy link
Copy Markdown
Member

Fixes#39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #39140.

I also experimented with adding nullable annotations to this type to get some additional error checking, but I ended up reverting that commit because it was out of scope of this work.

Marked draft because I haven't run any tests whatsoever over this code. It's just a demonstration of how I'm thinking of solving the problem.

Author:GrabYourPitchforks
Assignees:-
Labels:

area-Extensions-Primitives

Milestone:-

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs Outdated
Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@davidfowl

Copy link
Copy Markdown
Member

Performance tests!! 😄

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl There are lots of perf wins we could get from this type if we were willing to change some of its behavior. I leave that to a different issue.

@davidfowl

Copy link
Copy Markdown
Member

Yea, I just don't want regressions, this is in a hot path

@GrabYourPitchforks

GrabYourPitchforks commented Nov 20, 2020

Copy link
Copy Markdown
MemberAuthor

What methods specifically do you need optimized? The only one that is likely to regress is Equals(StringSegment), because it's now bouncing through an indirection. But I can hand-tune that if needed.

Reiterating my earlier comment: if this is really in a hot path, seriously consider allowing some behavioral changes here. For instance, the fact that there's special-casing all over the place to distinguish between default(StringSegment) and StringSegment.Empty leads to unnecessary complexity + slower perf. Allowing the two to be treated as equivalent would optimize things.

Allowing this change in behavior would make existing methods dumb wrappers around the already highly-optimized span versions. For example:

// Pretty much the whole call stack gets inlined into the caller at this point.publicboolEquals(StringSegmentother)=>this.AsSpan().SequenceEquals(other.AsSpan());

@davidfowl

Copy link
Copy Markdown
Member

I haven't looked deeply enough to tell if that change will have an impact m, I'm just saying these types are only used in ASP.NET Core so make sure you do some performance testing

Comment threadsrc/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs Outdated
@GrabYourPitchforks
GrabYourPitchforks marked this pull request as ready for review January 21, 2021 01:58
@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@maryamariyan@eerhardt this is now ready for review. I've added a bunch of unit tests to check various edge cases, especially with respect to globalization. I've also added extra argument checks around the Equals and Contains fast-paths to mirror the checks that string.Equals performs in those same scenarios. Finally, all unsafe or unsafe-equivalent code has been removed from this type.


fixed (char* p = Buffer)
int i;
for (i = span.Length - 1; (uint)i < (uint)span.Length; i--)

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.

(uint)i < (uint)span.Length

Is this micro optimization worth the mental exercise someone reading this code has to go through? Honestly, this might be the first time I've ever seen a loop written this way.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's to elide the bounds check, but we could write it using standard i >= 0 syntax if desired. Last I checked, the JIT still emitted the bounds check, which would make the "simple" way a perf regression compared to the existing code.

@eerhardteerhardtJan 22, 2021

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.

I'm definitely not an expert here, so double-check my work to make sure I didn't do anything wrong. But the current JIT'd code looks worse to me.

UPDATE: I found my first mistake - it defaults to Debug. Updated the JIT'd code to Release and they look the same to me.

https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAJVMA2E9IgYSIG88imTjSKAjAe04BsiBZTAApSABiJgAlEUbMGuZoqIA3MACciEAA5gAdkQC8EgHQBBCAGUduoZIDcspUQCWu4C4cKnRAGacNQs6GmtbGADIAproA5sAAFkQAtESYdkRCAK6uwJJBADzpWW6S2nrhUbFxac6JiZKOSvLeSs4+6QDGcerGAJIQAOpxzsARVmDtEUKlugDazgC6kvVezUxNq06oAOxEwGoZEZ4bTAC+DYpnK0rbvmA8EIcNl80NhCTkRFy8AugimOJSGRXdZOVQaabBMBmSzWWxHbzZDznZh+AJBIzTcoxeJJFLVIgAPiMomqtWWzRBq1aHS6al6AyGIzGEym1jmi3Jx0pxxIOz2B3hG2e3mFmx2PjuD0FREuJyAA

publicstaticboolM1(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;(uint)i<(uint)span.Length;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}publicstaticboolM2(stringa){varspan=a.AsSpan();inti;for(i=span.Length-1;i>=0;i--){if(char.IsWhiteSpace(span[i])){returntrue;}}returnfalse;}

The left side is M1 and the right is M2:
image

@GrabYourPitchforksGrabYourPitchforksJan 22, 2021

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

That's x86 debug output. I'm trying to get x64 release output but it'll take a few minutes.

Edit: Looks like x64 release is nearly identical between the two cases. I'll do the simple thing.

@eerhardteerhardt left a comment

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.

Just one comment on code that I found surprising.

LGTM. Nice work on filling out the unit tests for this type.

@GrabYourPitchforks

GrabYourPitchforks commented Jan 22, 2021

Copy link
Copy Markdown
MemberAuthor

@bbartels@eerhardt thanks both for your feedback! I've incorporated both of your suggestions and will merge after CI completes.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

I had forgotten that netfx (and netcore before 5.0) have some bugs w.r.t. the globalization IsPrefix and IsSuffix methods. I've suppressed on netfx the test cases which exercise those edge conditions.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

Opened #47374 to track CI failures. Continuing merge since CI failures are unrelated.

@GrabYourPitchforks
GrabYourPitchforks merged commit 19df292 into dotnet:masterJan 23, 2021
@GrabYourPitchforks
GrabYourPitchforks deleted the stringsegment branch January 23, 2021 19:40
@davidfowl

davidfowl commented Jan 24, 2021

Copy link
Copy Markdown
Member

@GrabYourPitchforks did you do any performance testing?

@sebastienros Lets keep an eye out here for regressions since ASP.NET Core is the only consumer of this.

@GrabYourPitchforks

Copy link
Copy Markdown
MemberAuthor

@davidfowl No formal benchmarking, but I looked at the codegen for hot methods like Equals and verified that things appear as expected. E2E testing will be more useful than microbenchmarking here.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringSegment.EndsWith inconsistent (buggy?) behavior

4 participants

@GrabYourPitchforks@davidfowl@eerhardt@bbartels