注意:问题标题已编辑 - 原始标题是关于优化“请批评 - 用于在 C 中转换字符串大小写的最佳函数”,这解释了为什么我的答案仅涉及优化而不是一般地“改进”功能。
如果您真的在寻找绝对最快的方法,那么从长远来看,无分支版本将是可行的方法,因为它可以使用 SIMD。此外,它还避免了使用表(如果内存真的很拥挤,在嵌入式系统上可能会太大)。
这是一个简单的非 SIMD 无分支示例,ToLower 是一个微不足道的变化。
char BranchFree_AsciiToUpper(char inchar)
{
// Branch-Free / No-Lookup
// toupper() for ASCII-only
const int ConvertVal = 'A' - 'a';
// Bits to Shift Arithmetic to Right : 9 == (char-bits + 1)
const int AsrBits = 9;
int c=(int)inchar;
//if( (('a'-1)<c) && (c<('z'+1)) ) { c += 'A'-'a'; }
int LowerBound = ('a'-1) - c;
int UpperBound = c - ('z' + 1);
int BranchFreeMask = (LowerBound & UpperBound)>>AsrBits;
c = c + (BranchFreeMask & ConvertVal);
return((char)c);
}
为了清晰起见,我的函数进行了扩展,并使用了非硬编码常量。你可以用硬编码值在一行中做同样的事情,但我喜欢可读的代码;但是,这是我算法的“压缩”版本。它并没有更快,因为它EXACT将同样的事情“压缩”到一行。
c+=(((96-(int)c)&((int)c-123))>>9)&(-32);
您可以在此处进行许多优化以使其更快。您可以为 ASCII 硬编码更优化的数字,因为该示例不假定除 a-z 和 A-Z 之外的任何编码映射是连续范围。例如对于 ASCII,如果您没有桶形移位器,您实际上可以将 AsrBits 更改为 4 (9-5),因为 ConvertVal 将是 +/-32,具体取决于 toupper 或 tolower 操作。
一旦您有了工作的无分支版本,您可以使用 SIMD 或位旋转 SWAR(寄存器内的 SIMD)技术一次转换 4-16 个字节(甚至可能更多取决于您的寄存器的宽度以及您是否展开以隐藏延迟)。这将比任何几乎仅限于单字节转换的查找方法快得多,除非您有非常大的表,这些表同时处理的每个字节呈指数增长。
此外,您可以在不使用 int 向上转换的情况下生成无分支谓词,但随后您必须执行更多操作(向上转换只是每个范围的减法)。您可能需要对 SWAR 进行扩展操作,但大多数 SIMD 实现都有一个比较操作,可以免费为您生成掩码。
SWAR/SIMD 操作还可以从更少的内存读取/写入中受益,并且确实发生的写入可以对齐。这在具有加载命中存储惩罚的处理器(例如 PS3 单元处理器)上要快得多。将此与展开版本中的简单预取相结合,您几乎可以完全避免内存停滞。
我知道我的示例中似乎有很多代码,但有 ZERO 分支(隐式或显式),因此没有分支错误预测。如果您在一个具有严重分支错误预测惩罚的平台上(这对于许多流水线嵌入式处理器来说都是如此),那么即使没有 SIMD,您的 上述代码的优化发布版本应该比看起来不那么复杂但创建的代码运行得更快隐式分支.
即使没有 SIMD/SWAR,智能编译器也可以展开和交错上述实现以隐藏延迟并产生非常快的版本 - 特别是在现代超标量处理器上,每个处理器可以发出多个非依赖指令循环。这通常不适用于任何分支版本。
如果您手动展开,我会将加载和收集存储进行分组,以使编译器更容易在它们之间交错非分支非依赖指令。示例:
// Unrolled inner loop where 'char *c' is the string we're converting
char c0=c[0],c1=c[1],c2=c[2],c3=c[3]; // Grouped-Loads
c[0]=BranchFree_AsciiToUpper(c0);
c[1]=BranchFree_AsciiToUpper(c1);
c[2]=BranchFree_AsciiToUpper(c2);
c[3]=BranchFree_AsciiToUpper(c3);
c+=4;
一个体面的编译器应该能够内联 ToUpper 并完全交错上述代码,因为没有分支,没有内存别名,并且每个指令之间没有相互依赖的指令。只是为了好玩,我决定实际编译它,一个针对 PowerPC 的编译器为双问题超标量内核生成了完美的交错,它很容易胜过任何带有分支的代码。
mr r31,r3
mr r13,r13
lbz r11,0(r31)
lbz r10,1(r31)
extsb r11,r11
lbz r9,2(r31)
extsb r10,r10
lbz r8,3(r31)
subfic r7,r11,96
addi r6,r11,-123
srawi r5,r7,9
srawi r4,r6,9
subfic r3,r10,96
addi r7,r10,-123
extsb r9,r9
srawi r6,r3,9
srawi r3,r7,9
subfic r7,r9,96
addi r30,r9,-123
extsb r8,r8
srawi r7,r7,9
srawi r30,r30,9
subfic r29,r8,96
addi r28,r8,-123
srawi r29,r29,9
srawi r28,r28,9
and r5,r5,r4
and r3,r6,r3
and r7,r7,r30
and r30,r29,r28
clrrwi r4,r5,5
clrrwi r6,r7,5
clrrwi r5,r3,5
clrrwi r7,r30,5
add r4,r4,r11
add r3,r5,r10
add r11,r6,r9
stb r4,0(r31)
add r10,r7,r8
stb r3,1(r31)
stb r11,2(r31)
stb r10,3(r31)
证据在布丁中,与分支版本相比,即使在使用 SWAR 或 SIMD 之前,上述编译的代码也会非常快。
总而言之,这应该是最快方法的原因:
- 没有分支预测错误的惩罚
- 能够一次对 4-16 个(或更多)字节进行 SIMD 化算法
- 编译器(或程序员)可以展开和交错以消除延迟并利用超标量(多问题)处理器
- 没有内存延迟(即查表)