-
Notifications
You must be signed in to change notification settings - Fork 1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
Switching ReadOnlySpan<char> with constant strings #1881
Comments
This does seem really annoying. I'm not sure I have a language proposal that would fix this, but it would be something I'd like to address. The obvious route would be something like an implicit conversion on |
Yes it does exist in .NET Core 2.1 (NOT with the portable version!!) If this was implemented, this should work with the portable version too. |
Note: is there an idea on what the codegen here would actually be? The normal string-switch works by building a dictionary and switching on that. That likely wouldn't work for ReadOnlySpans. Perhaps emitting code for an inline-trie might be appropriate? |
Why not? As far as I can tell, it doesn't actually use |
@CyrusNajmabadi @svick |
I'd like for |
Oh terrific. Good to know! It does seem a bit interesting to me that it both has to scan the string to produce the hash, and then has to do the equality check again. It feels like a double pass of the string, when only one would be necessary since you're generating the code here manually. But maybe it would be a bit too much codebloat, or too branchy, when this can be very simple codegen. |
@CyrusNajmabadi the comparison is necessary. you can make a perfect hash function to avoid collisions among your cases, but it's still possible for your input string to not be one of the switch options. We'd need something akin to VC++'s |
@scalablecory I understand the need for the compare if you go the hash route. I was saying that i was suprised that the hash-route was used in the first place, isntead of just codegening something like a trie. |
It states that a trie would be slower than a HashSet when searching entire strings. A trie would be faster if we are using pattern searching, but switches are not for that. |
There would be no insertion operations. The compiler wouldn't be populating a generic trie container, it would be generating the trie-comparisons directly based on the literal strings provided. |
Your link says this:
|
yes. that's what i'm trying to say. Effectivley, right now the compiler has to walk every character of the string, to produce the hash. It then has to switch on the hash, and once done, the runtime has to then walk every character of the string a second time. This is basically the poster case for when you want a trie. Instead of having two loops of the string, you just walk each character of it one at a time, and you either end up exactly in the matching branch, or you fall out into the default branch label. |
@HaloFour @CyrusNajmabadi I am talking about trie vs HashSet, not trie vs List or trie vs SortedList!!! |
I don't see how it's relevant. It's not talkign about what i was talking about. Specifically that's a benchmark of a very specific trie impl against a hashset. Importantly, it's a trie actually built of in-memory objects. So you're incurring all sorts of hits trying to walk that. I'm talking about a trie represented as code. These are entirely different things. It would be like me complaining about the hashing that the compiler does here because of some aspect of a HashSet. They're entirely different beasts, even though they both involve 'hashing'. |
Ok. I actually did a perf comparison of the two. My Trie version beats the Standard C# compiler hashing version by about 2.2x. Suprisingly, i made an unsafe version as well... but it performed worse than the safe version. The numbers came out to:
The test worked by generating switches using the top hundred most common english words. It then performed the switch using the top thousand words (so 10% hitrate, 90% miss rate). Codefiles of interest: HashSwitch There are very likely opportunities for improvement here. I didn't spend much time optimizing anything. |
Note: the numbers are even worse for hashing if i go for a 100% hitrate on the switch:
Here the hashing version is 2.8x worse. This is unsurprising as the hashing version will definitely do better on misses than matches (after all, on a match, it has to actually then compare the strings, but on a miss, it can bail without any string compare). However, even with the boost it gets from misses, it's still more than 2x slower than the trie version. |
@CyrusNajmabadi Isn't that off-topic in this issue? Maybe it would make sense to create a new issue about that in the roslyn repo? |
@svick Yes. It's arguably off topic in that the language shouldn't have to care about the impl details. however, i thought it was a bit worthwhile to understand if there were at least feasible implementation strategies. I assume if we wanted switching over readonlyspans, we'd want to know if it could be done efficiently. So i thought this exploration was valuable in terms of demonstrating that that was the case :) This wasn't an attempt to say: C# should switch (har har) to this. It was more: this language feature can sensibly be implemented, and here are some good perf results to demonstrate that it likely would work well and would not conflict with the goals of C# or ReadOnlySpans. Cheers! |
@CyrusNajmabadi How would cases with conditions play with the trie implementation? switch(someReadOnlySpan)
{
case "\u0444":
return 444;
case var inSomeList when someList.Contains(inSomeList[0]):
Console.Write(inSomeList[0]);
return (int)inSomeList[0];
case "\u0456":
return 456;
} If '\u0456' is in someList, then it will be written to the console. |
You'd need a separate trie before and after the center case, since the center case makes the switch become order-dependent. Imagine splitting into |
Welp, that would be a new design guideline for me. |
We might want to enable this with ReadOnlyMemory<char> and Memory<char> too. |
Any update? I would really like this feature. |
I'll bring this up at the LDM next time we triage. |
My first suggestion that gets championed!! ❤️😆 |
It doesn't work with Utf8String: public int Foo(ReadOnlySpan<byte> s)
{
return s switch
{
"foo"u8 => 1,
"bar"u8 => 2,
_ => 0
};
} error CS0150: A constant value is expected |
Correct. We did not enable this feature for Utf8Strings, as they are not constants. |
That seems like a great opportunity to improve the feature. Could supported pattern expressions be expanded to include UTF8 strings when they are expressed as literals (and only when)? |
This doesn't make sense to me. Why are UTF8 string literals not constants, according to the language? |
Logically they are constants, but:
|
That feels like circular logic -- utf8 literals can't be constants because they're not defined as constants. Pure language rules. As it stands it doesn't seem like a good justification. There are good reasons to make these constants and I haven't seen any reasons not to. Also, I don't think it has to be the case that all constant values have to be allowed as optional parameter values. That's another language rule that can be changed if implementation restrictions require it. |
And pratically they are not constants: using System;
using System.Runtime.CompilerServices;
ReadOnlySpan<byte> x = "Hallo"u8;
Console.WriteLine(x[0]); //72
ref readonly var b = ref x[0];
ref var bwrite = ref Unsafe.AsRef(in b);
bwrite = 7;
Console.WriteLine(b); //7
Console.WriteLine(bwrite); //7
Console.WriteLine(x[0]); //7 As written in the proposal, those strings / bytes are stored in the |
If you want to go unsafe, regular strings are not "constant" either. var x ="Hallo";
fixed(char* p = x) p[0]='X';
Console.Write("Hallo"); But I think that's besides the point. I understand #1881 (comment) as a call for a feature to make more types viable for a constant (including span or other structs) which will cover utf8 strings as a side effect. That would be still different from making the utf8 string literal itself a constant as that may require special handling - this issue. (There's a few other proposals to allow |
For string it is an implementation detail - they are actually not allocated in ro memory sections.
Maybe this should be added to the spec for UT8-literals, too. |
Even simpler: unsafe {
byte* datas = stackalloc byte[]
{
1,2,3,4,5,
};
ReadOnlySpan<byte> x = new(datas, 5);
Console.WriteLine(datas[0]); //1
Console.WriteLine(x[0]); //1
datas[0] = 7;
Console.WriteLine(datas[0]); //7
Console.WriteLine(x[0]); //7
} ROS isn't a frozen collection, it's just a read only view on some datas. |
@FaustVX You're misunderstanding the proposal. It's not to allow all And yes, I regard implementation restrictions, like what's allowed in metadata, as being questions about the language implementation, not the language definition (the spec). If the use cases we're thinking of (switching) are completely unimplementable -- that's a good reason not to do it in the language. But if there simply need to be some restrictions on where they can go (e.g., not allowed in optional parameters), that seems fine to me. |
it def feels (to me) at least) that there's some space for value to be constant even if the type it is isn't always a constant. char[] chars = { 'a', 'b', 'c' };
ReadOnlySpan<char> span = chars.AsSpan(); However, in a cast where there the value is a literal, we could make the claim it's a constant. So this would be ok: const ReadOnlySpan<byte> ConstantString = "abc"u8; |
Right, to be clear, that's how all constants work. There are no "constant types" in the language, there are only constant values, and it happens to be the case that those values are only of certain types. That is,
Moreover, allowing |
(btw this issue is about switching, but I don't see any reason why we can't allow utf8 string literals in optional parameters either. It would be implemented like DecimalConstantAttribute) |
@agocke Good points. Thanks! I"m good with championing this alongside Julien :) |
One more thing that I thought of. Making this change has some interesting implications for other types. In particular, this would be the only way in the language to produce a constant-expr byte string, and the only way to match against a constant-expr byte string. Depending on which particular byte string you're using, I could see people encoding it in a UTF8 string (assuming that it's valid UTF8) just so they could use the constant-expr functionality. Honestly, it seems like a pretty reasonable usage. I wouldn't try to block this, but instead look it is as potentially another language deficiency -- there's currently no way to produce a constant byte array in the language. I know there's a separate working group looking at new literals for lists and dictionaries. It might be interesting to either explore one of those syntaxes producing a constant expression. Alternatively, it might be possible to make |
Actually, even the workaround that the OP's suggests is not correct. The
We have to use
We do need a better way to compare spans with strings, with lees hoop-jumping |
Are there any other places in the language where To me at least, I imagine this probably comes with its own strings attached but I thought I'd at least toss it out there. I'm definitely interested in hearing about any potential drawbacks to this solution. I'm not an expert in this space. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Speclet: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/pattern-match-span-of-char-on-string.md
With the recent addition of Spans, we can reduce a lot of allocations when dealing with strings. For example, substrings can be replaced with slices. However, there is one place where using strings is better:
With spans, you are forced to write boilerplate code:
Which is identical to the if-else mess that switches aim to solve.
It would be better if spans can be switched with constant strings.
Special-casing spans as a compiler-known type already have precedent in stackalloc expressions, it can also be done here.
Possible extension to regular Span<char> can also be considered, but it is not as important as ReadOnlySpan<char>. We could also enable this with ReadOnlyMemory<char> and Memory<char>.
More possible extensions: switching (ReadOnly)Span<char> with char, (ReadOnly)Span<T> with T where T is a constant type.
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-23.md#pattern-matching-over-spanchar
The text was updated successfully, but these errors were encountered: