C# 检查一个字符是否等于多个其他字符,并尽可能减少分支

C# 检查一个字符是否等于多个其他字符,并尽可能减少分支,c#,.net,performance,math,bit-manipulation,C#,.net,Performance,Math,Bit Manipulation,我正在编写一些性能敏感的C#代码,用于处理字符比较。我最近发现了一个技巧,你可以判断一个字符是否等于一个或多个没有分支的字符,如果它们之间的差值是2的幂 例如,假设您想检查字符是U+0020(空格)还是U+00A0(非中断空格)。由于两者之间的差异为0x80,因此可以执行以下操作: public static bool Is20OrA0(char c) => (c | 0x80) == 0xA0; 与此幼稚的实现相反,如果角色不是空格,则会添加额外的分支: public static b

我正在编写一些性能敏感的C#代码,用于处理字符比较。我最近发现了一个技巧,你可以判断一个字符是否等于一个或多个没有分支的字符,如果它们之间的差值是2的幂

例如,假设您想检查字符是U+0020(空格)还是U+00A0(非中断空格)。由于两者之间的差异为0x80,因此可以执行以下操作:

public static bool Is20OrA0(char c) => (c | 0x80) == 0xA0;
与此幼稚的实现相反,如果角色不是空格,则会添加额外的分支:

public static bool Is20OrA0(char c) => c == 0x20 || c == 0xA0;
第一个是如何工作的,因为两个字符之间的差是2的幂,所以它正好有一个位集。这意味着,当你或它与角色一起,并导致一个特定的结果时,有2^1个不同的字符可能导致这个结果

不管怎样,我的问题是,这个技巧是否可以扩展到差异不是2的倍数的字符?例如,如果我有字符
#
0
(顺便问一下,它们之间的差值为13),是否有任何类型的位旋转黑客可以用来检查字符是否等于它们中的任何一个,而不进行分支

谢谢你的帮助


编辑:作为参考,这是我第一次在.NET Framework源代码的
char.isleter
中偶然发现这个技巧的地方。它们利用了
a-a==97-65==32
这一事实,并简单地用0x20或其大写字符(与调用
ToUpper
)进行比较。

您可以使用相同的技巧与一组2^N值进行比较,前提是它们除N位之外的所有其他位都相等。例如,如果值集为0x01、0x03、0x81、0x83,则N=2,您可以使用
(c | 0x82)==0x83
。请注意,集合中的值仅在第1位和/或第7位不同。所有其他位都相等。这种优化可以应用的情况并不多,但如果可以,并且每一点额外的速度都很重要,那么这就是一个很好的优化

这与优化布尔表达式的方式相同(例如,在编译VHDL时)。您可能还需要查找卡诺地图

也就是说,对字符值进行这种比较是非常糟糕的做法,尤其是使用Unicode,除非您知道自己正在做什么,并且正在做一些非常低级的事情(例如驱动程序、内核代码等)。比较字符(与字节相反)必须考虑语言特征(如大写/小写、连字、重音、合成字符等)


另一方面,如果您只需要二进制比较(或分类),则可以使用查找表。对于单字节字符集,这些字符集可以相当小且非常快。

如果您可以容忍乘法而不是分支,并且您测试的值只占用您使用的数据类型的较低位(因此,当乘以一个小的常数时,它不会溢出,如果考虑到一个较大的数据类型,并且使用一个相应的较大的掩码值,如果这是一个问题),那么你可以将该值乘以一个常数,迫使这两个值为2的功率。

例如,在
#
0
(十进制值35和48)的情况下,两个值相距13。向下舍入,2到13的最接近幂为8,即13的0.615384615。将其乘以256并向上舍入,得到8.8的固定点值,即158

以下是35和48乘以158的二进制值及其相邻值:

34 * 158 = 5372 = 0001 0100 1111 1100
35 * 158 = 5530 = 0001 0101 1001 1010
36 * 158 = 5688 = 0001 0110 0011 1000

47 * 158 = 7426 = 0001 1101 0000 0010
48 * 158 = 7548 = 0001 1101 1010 0000
49 * 158 = 7742 = 0001 1110 0011 1110
较低的7位可以忽略,因为它们不是相互分离任何相邻值所必需的,除此之外,值5530和7548仅在第11位不同,因此您可以使用掩码和比较技术,但使用and而不是OR。二进制掩码值为
11110111 1000 0000
(63360),比较值为
0001 0101 1000 0000
(5504),因此可以使用此代码:

public static bool Is23Or30(char c) => ((c * 158) & 63360) == 5504;
我还没有分析过这个,所以我不能保证它比简单的比较快


如果您确实实现了类似的功能,请务必编写一些测试代码,循环遍历可以传递给函数的每个可能值,以验证它是否按预期工作。

如果没有分支确实是您的主要问题,您可以这样做:

if ( (x-c0|c0-x) & (x-c1|c1-x) & ... & (x-cn|cn-x) & 0x80) {
  // x is not equal to any ci

如果x不等于某个特定的c,则x-c或c-x都将为负,因此x-c | c-x将设置位7。这应适用于有符号和无符号字符。如果对所有c设置&it,则仅当为每个c设置位7时,结果才会设置位7(即x不等于任何一个)

您的第一个代码片段已经不正确-它根本无法工作。
c&0x80
(仅保留一个设置位)怎么可能等于
0xa0
(包含两个设置位)?我觉得你应该首先关注正确性…@JonSkeet-Whoops,意思是写
而不是
&
。会修复的,谢谢你指出。用一种奇特的方法,
&
的拼写错误吗?分支有什么不对吗?我对分支知之甚少,但它会避免分支到我们这里吗e改为非短路运算符,即
=>c=='#'| c=='0'
?c中的字符为16位,因此需要测试位15