Skip to content

Conversation

@Joy-less
Copy link
Contributor

There are two reasonable approaches to replacing a value with another value of a different length:

  1. Find every index of oldValue, then create a bigger/smaller buffer once and insert the values.
  2. Find each index of oldValue, replacing with newValue and resizing the buffer each time.

Previously, the replace operation did a combination of the two which seemed over-the-top. I changed it to only use the second approach, which is far simpler and gives better performance!

BEFORE:

Method Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
DotNetStringBuilder 18.66 us 0.266 us 0.249 us 1.00 0.02 9.4604 29.08 KB 1.00
ValueStringBuilder 122.50 us 0.732 us 0.685 us 6.57 0.09 4.8828 15.24 KB 0.52

AFTER:

Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
DotNetStringBuilder 18.34 us 0.177 us 0.166 us 1.00 9.4604 29.08 KB 1.00
ValueStringBuilder 19.39 us 0.184 us 0.163 us 1.06 4.3945 13.55 KB 0.47

Note:
Two of the tests had to be changed because, in my opinion, you shouldn't be allowed to give out-of-bounds parameters just because another parameter is empty and short-circuits.

{
Span<char> tempBuffer = stackalloc char[24];
if (spanFormattable.TryFormat(tempBuffer, out var written, default, null))
Span<char> tempBuffer = stackalloc char[128];
Copy link
Owner

Choose a reason for hiding this comment

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

Is there any ISpanFormattable where the string representation is that long? (Given that you cannot pass in a IFormatProvider).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know but users can make their own ISpanFormattable types, so the fallback to ToString is required. However, 128 chars might be too much, it's up to you if you'd like to change it (I just went with the number used elsewhere in the repository)

Copy link
Owner

Choose a reason for hiding this comment

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

That is a good point. The 24 was used as even decimal and double will not take more space than that when no custom provider is used.

I don’t see problem per-se with 128 bytes. I am no embedded guy to judge it might be too much. We could also settle on 64 byte or 32 and raise it once we have complaints :)

Copy link
Contributor Author

@Joy-less Joy-less Feb 21, 2025

Choose a reason for hiding this comment

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

Yeah, it should make barely any difference performance-wise since it's only 256 bytes and very short-lived. I think 64 chars should work fine as well.
Also, should IFormatProvider? be added as an argument?

Copy link
Owner

Choose a reason for hiding this comment

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

Interesting questio. If so I wouldn’t make it optional and provably would ask the caller about the size.

So there would an additional overload. I don’t think much would speak against is, as we only have to pass it through

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm thinking that asking the caller about the size would be redundant, because it's not recommended to stackalloc more than 256 bytes anyway. It's up to you if you want a new overload or an optional argument though. Bare in mind that double.TryFormat (among others) use an optional format provider argument (likely to implement the interface).

Copy link
Owner

Choose a reason for hiding this comment

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

That is true - we have to check if it is bigger than a certain size and have to resort to an ArrayPool if so.
Unfortunately the Append method doesn’t do that right now and would crash the application :) so therefore good catch.

If we go down that route, also the append method should get the IFormatProvider bit.

Yeah probably it is okay to stick to that pattern. It isn’t more explicit just because you have to set it in another overload. I am not sure if we can assume that with a custom formatprovider the written span will always be less than a certain size.

Copy link
Owner

@linkdotnet linkdotnet left a comment

Choose a reason for hiding this comment

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

Really cool work! Added two smaller things and please add yourself to the CHANGELOG.md - I'd make directly a new version

@Joy-less
Copy link
Contributor Author

I also added scoped to the ReplaceGeneric oldValue argument. I noticed that ReplaceGeneric can simply be renamed to Replace, would you like this? @linkdotnet

@linkdotnet
Copy link
Owner

I also added scoped to the ReplaceGeneric oldValue argument. I noticed that ReplaceGeneric can simply be renamed to Replace, would you like this? @linkdotnet

Really? I thought it might resort to a boxed invocation. We can also do this for v3 if that isn’t the case. For now, because of semantic versioning, I would like to keep it as is. The scoped though is absolutely fine :)

@sonarqubecloud
Copy link

@Joy-less
Copy link
Contributor Author

I made some minor clarity changes. It's ready to be merged unless you have more concerns.

@linkdotnet linkdotnet merged commit 21d99d2 into linkdotnet:main Feb 21, 2025
3 checks passed
@Joy-less Joy-less deleted the simplify-replace branch February 21, 2025 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants