如何改进这些案例转换功能?

时间:2010-10-07 17:01:14

标签: c string optimization case-sensitive

作为一个学习练习,我的三个函数-ToggleCase,LowerCase和UpperCase-每个都期望一个指向ASCII字符串的指针,由空字符终止;他们按预期工作。有更高效或更快的方法来完成这项任务吗?我是否违反任何优秀C编码的潜规则?我已经使用了宏,因为我认为它使代码看起来更好,而且比函数调用更有效。这是典型的还是矫枉过正的?

请随意挑选并批评代码(但确实很好)。

case_conversion.h

#define CASE_FLAG 32
#define a_z(c) (c >= 'a' && c <= 'z')
#define A_Z(c) (c >= 'A' && c <= 'Z')

void ToggleCase(char* c);
void LowerCase(char* c);
void UpperCase(char* c);

case_conversion.c

#include "case_conversion.h"

void ToggleCase(char* c)
{
 while (*c)
 {
  *c ^= a_z(*c) || A_Z(*c) ? CASE_FLAG : 0;
  c++;
 }
}
void LowerCase(char* c)
{
 while (*c)
 {
  *c ^= A_Z(*c) ? CASE_FLAG : 0;
  c++;
 }
}
void UpperCase(char* c)
{
 while (*c)
 {
  *c ^= a_z(*c) ? CASE_FLAG : 0;
  c++;
 }
}

12 个答案:

答案 0 :(得分:15)

我的最爱:

*c += (*c-'A'<26U)<<5; /* lowercase */
*c -= (*c-'a'<26U)<<5; /* uppercase */
*c ^= ((*c|32U)-'a'<26)<<5; /* toggle case */

由于您的目标将是嵌入式系统,您应该学习如何消除不必要的代码膨胀,分支等。确定ascii字符是否按字母顺序排列的条件是4个比较/分支操作;我是1.我建议在算术和位操作技巧上查找一些很好的资源。

注意:我在发布答案后将*32操作更改为<<5,因为许多嵌入式系统编译器太差而无法为您执行此操作。在为优秀的编译器编写代码时,*32可能会更好地说明您的意图。

编辑:关于我的代码有太多隐式编译器生成的操作的指控,我认为这完全是假的。这是伪asm任何半正式编译器应为第一行生成的:

  1. 加载*c并对其进行零或符号扩展,以填充int大小的字词(取决于普通char是否已签名或无签名)。
  2. 使用无符号(非溢出陷阱)sub指令减去常量26。
  3. 如果未设置进位标志,则条件跳转到其余代码。
  4. 否则,在*c
  5. 的值处添加32

    步骤2和3可以组合在使用比较 - 跳转操作而不是标志的体系结构上。我能看到任何重大幕后成本的唯一方法就是机器不能直接寻址字符,或者它是否使用了令人讨厌的(符号/幅度或补码)符号值表示,在这种情况下转换为无符号会是不平凡的。据我所知,现代嵌入式架构没有这些问题;它们主要与传统大型机(以及较小程度上的DSP)隔离。

    如果有人担心错误的编译器实际执行<<5的算术运算,您可以尝试:

    if (*c-'A'<26U) *c+=32;
    

    而不是我的代码。无论如何,这可能更干净,但我通常喜欢避免语句,因此我可以将代码推送到循环条件或类似函数的宏中。

    编辑2:按要求,第一行的无分支版本:

    <击> *c += (64U-*c & *c-91U)>>(CHAR_BIT*sizeof(unsigned)-5);

    *c += (64U-*c & *c-91U) >> CHAR_BIT*sizeof(unsigned)-1 << 5;

    为了使其可靠地运作,c应该具有unsigned char *类型,unsigned int应该比unsigned char严格更宽。

答案 1 :(得分:9)

您的宏至少存在两个主要问题。考虑如果我将其中一个称为

,会发生什么
a_z('a' + 1);

由于运营商优先权,呼叫不会给出正确的结果。使用括号很容易解决这个问题:

#define a_z(c) ((c) >= 'a' && (c) <= 'z')

但也可以像这样调用它们:

a_z(i++);

此调用将增加i两次!这在宏中并不容易解决(如果有的话)。我建议使用内联函数(如果需要,请参见下文)。

在大写/小写之间转换的最快方法我知道使用查找表。当然,这会以速度换取记忆 - 在了解您的特定平台时选择您的偏好: - )

您需要两个阵列,一个用于任一方向。像

一样初始化它们
char toUpper[128]; // we care only about standard ASCII
for (int i = 0; i < 128; i++)
  toUpper[i] = i;
toUpper['a'] = 'A';
...
toUpper['z'] = 'Z';

转换是微不足道的:

char toUpperCase(char c)
{
  return toUpper[c];
}

(对于生产代码,应该对此进行改进,以将数组扩展到给定平台上的所有可能char值(或将其缩小为合法值并进行参数检查),但为了说明,这样做。)

答案 2 :(得分:5)

注意:问题标题已被编辑 - 原始标题是关于优化“请批判 - 用于转换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之外的任何编码映射都是连续的范围。例如,如果您没有桶形移位器,您实际上可以将AsrBits更改为4(9-5),因为ConvertVal将为+/- 32,具体取决于toupper或tolower操作。

一旦你有分支免费版本,你可以使用SIMD 或bit-twiddling SWAR(寄存器内的SIMD)技术一次转换4-16个字节(甚至可能更多地取决于您的寄存器的宽度以及是否展开以隐藏延迟)。这将比任何查找方法快得多,后者几乎只限于单字节转换,除非你有非常大的表,每个字节同时处理指数增长。

此外,您可以在不使用int upcast的情况下生成branchfree谓词,但是您必须执行更多操作(向上转换它只是每个范围减去一个)。您可能需要对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之前都会非常快。

总之,为什么这应该是最快的方法:

  1. 没有分支错误预测处罚
  2. 能够一次4-16(或更多)字节的SIMD-ify算法
  3. 编译器(或程序员)可以展开和交错以消除延迟并利用超标量(多发行)处理器
  4. 没有内存延迟(即表查找)

答案 3 :(得分:2)

好的,这里有。在此选项卡上书写...在另一个选项卡上滚动您的代码: - )

  1. #define a_z(c) (c >= 'a' && c <= 'z')

    • 像宏这样的函数的名称应该是全部大写(可能是IS_LOWERCASE),以提醒用户它是一个宏
    • 扩展中的
    • c应该在括号内,以防止出现奇怪的副作用
    • 个人选择:我喜欢重新排序条件,使其更像英语'a'&lt; = c&lt; ='z',如(('a' <= (c)) && ((c) <= 'z'))
  2. 我会让函数void ToggleCase(char* c)返回char*(与发送时相同),以便能够按顺序使用它们:printf("%s\n", UpperCase(LowerCase("FooBar")));

  3. 源代码

    1. 三元运算符不会使您的代码更快或更容易阅读。我写了一个简单的if
    2. 就是这样。

      哦!还有一件事:你的代码假定为ASCII(你自己这么说),但没有记录。我会在头文件中添加一个关于它的注释。

答案 4 :(得分:2)

答案 5 :(得分:1)

也许我是派对的人,因为据说这是一次学习练习,但学习的关键部分应该是学会有效地使用你的工具。

ANSI C包含标准库中的必要功能,并且可能是编译器供应商为您的体系结构进行了大量优化。

标准头文件ctype.h包含函数tolower()和toupper()。

答案 6 :(得分:0)

首先,我要将a_zA_Z重命名为is_ASCII_Lowercaseis_ASCII_Uppercase。它不像C-ish,但它更容易理解。

此外,^=?:的使用有效,但我发现它的可读性低于简单的if语句。

答案 7 :(得分:0)

怎么样(几乎可以):

char slot[] = { 0, 31, 63, 63 };
*c = slot[*c/32] + *c%32;

你可以改变一些事情:

*c += a_z(*c)*CASE_FLAG; // adds either zero or three two
// you could also replace multiplication with the shift (1<<5) trick

字符串实际上是数组:

char upper[] = "ABC..ABC..."; // 
...
*c = upper[*c+offset];

char upper[] = "ABC.."; // 
...
*c = upper[*c%32];

*c = 'A' + *c%32;

或其他......

答案 8 :(得分:0)

也许我花了太多时间使用C ++而C还不够,但我不是那些有参数的宏的粉丝...正如Peter Torok所指出的那样,他们可能会导致一些问题。您对CASE_FLAG的定义是可以的(它不带任何参数),但我会用函数替换宏a_z和A_Z。

答案 9 :(得分:0)

我的方法是“仅在需要时修剪”。

根据您的系统和您的cpu架构,可以采用不同的方式完成许多工作。

我对你的代码有一些设计要点。首先,宏。宏有一些残酷的陷阱,应谨慎使用。第二,使用全局来切换案例。我会改写看起来像这样 -

 enum CASE {UPPER, LOWER};

void ToggleCase(char* c, CASE newcase)
{
    if(newcase == UPPER)
       UpperCase(c);
    else if(newcase == LOWER)
       LowerCase(c);
    else 
       { ; } //null
}

在微效率意义上,每次调用增加大约1个额外指令。还有一些分支可能发生,这可能会导致缓存未命中。

void LowerCase(char* c)
{
  while (*c++)  //standard idiom for moving through a string.
  {
    *c = *c < 'Z' ? *c + 32 : *c;
  }
}


void UpperCase(char* c)
{
  while (*c++)
  {
    *c = *c > 'a' ? *c - 32 : *c;
  }
}

现在,对我的代码有一些批评。

首先,它很狡猾。其次,它假设输入是[a-zA-Z] +。第三,它只是ASCII(EBDIC怎么样?)。第四,它假设为null终止(某些字符串在字符串的开头有一些字符 - 我认为是Pascal)。第五,代码上/下是不是100%天真明显。另请注意,ENUM是一个错误的整数。您可以传递ToggleCase("some string", 1024)并进行编译。

这些事情并不是说我的代码非常糟糕。它服务并将服务 - 只是在某些条件下。

答案 10 :(得分:0)

  

我已经使用了宏,因为我认为它使代码看起来更好,而且比函数调用更有效。

效率更高吗?您对代码大小有什么要求? (对于生成的可执行代码,而不是C源代码。)在现代桌面系统上,这很少是问题,速度更重要;但除了“嵌入式系统应用程序”之外,您还没有给我们任何更多细节,因此我们无法为您解答此问题。但是,这不是问题,因为宏中的代码真的很小 - 但你不能认为避免函数调用总是更有效!

如果允许,您可以使用内联函数。自99年以来,它们已经正式成为C的一部分,但在几个编译器中得到了更长时间的支持。内联函数 比宏更清晰,但是,再次根据您的确切目标要求,可能很难从源中预测生成的代码。然而,更常见的是,人们仍然处于过时(现在超过十年!)不支持它们的C编译器。

简而言之,您必须始终了解确切要求,以确定最佳选择。然后你必须test to verify your performance predictions

答案 11 :(得分:0)

如果一个人试图同时处理多个字节,我认为最好的方法是强制所有值为0..127,加5或37(这会使'z'变为'Z'变为127) ,注意该值,然后添加26,注意该值,然后做一些修改。类似的东西:

unsigned long long orig,t1,t2,result;

t1 = (orig & 0x7F7F7F7F7F7F7F7F) + 0x0505050505050505;
t2 = t1 + 0x1A1A1A1A1A1A1A1A;
result = orig ^ ((~(orig | t1) & t2 & 0x8080808080808080) >> 2);
嗯......我想这很好,即使适用于32位机器也是如此。如果四个寄存器预先加载了适当的常量,ARM可以用最优代码执行操作,七个指令需要七个周期;我怀疑编译器是否会找到优化(或者认为保持寄存器中的常量会有所帮助 - 如果常量不保存在寄存器中,单独处理字节会更快)。