检查一个字符是否等于多个其他字符,尽可能少地进行分支



我正在编写一些性能敏感的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的幂,所以它只有一个比特集。这意味着,当你将其与字符进行OR运算并得出某个结果时,正好有2^1个不同的字符可能会得出该结果。

无论如何,我的问题是,这个技巧能以某种方式扩展到差异不是2的倍数的角色吗?例如,如果我有字符#0(顺便说一句,这两个字符相差13),有没有什么可以用来检查char是否等于它们中的任何一个,而不需要分支?

谢谢你的帮助。

edit:作为参考,这里是我第一次在char.IsLetter中的.NET Framework源代码中偶然发现这个技巧的地方。他们利用了a - A == 97 - 65 == 32这一事实,并简单地将其与0x20进行"或"运算以大写char(而不是调用ToUpper)。

如果你可以容忍乘法而不是分支,并且你正在测试的值只占用你正在使用的数据类型的低位(因此当乘以一个小常数时不会溢出,如果这是一个问题,可以考虑转换为一个更大的数据类型并使用一个相应的更大的掩码值),然后你可以把这个值乘以一个常数,迫使这两个值相差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。二进制中的掩码值为1111 0111 1000 0000(63360),比较值为0001 0101 1000 0000(5504),因此可以使用以下代码:

public static bool Is23Or30(char c) => ((c * 158) & 63360) == 5504;

我还没有对此进行分析,所以我不能保证它比简单的比较更快。

如果你确实实现了这样的东西,一定要编写一些测试代码,循环遍历可以传递给函数的每个可能的值,以验证它是否按预期工作。

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

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

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

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

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

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,只有为每个c设置了位7(即x不等于其中任何一个),

才会设置位7

最新更新