【问题标题】:How could these case conversion functions be improved?如何改进这些大小写转换功能?
【发布时间】:2010-10-07 17:01:13
【问题描述】:

作为一个学习练习,我的三个函数——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++;
 }
}

【问题讨论】:

  • 查找表会更快。此外,for (int i = strlen(s); i; i--) 可能会比while (*c) 更快(至少在优化strlen 的情况下)。
  • @ruslik - 你会以什么方式优化 strlen() 使其比简单地增加指针更快?特别是因为无论如何您都必须增加指针(或进行索引取消引用)?
  • 您假设大小写字母之间存在一位差异。这不适用于每个字符集。另外假设英语是您需要支持的唯一语言,这在文化上有点不敏感。
  • @Martin:事实上,ASCII 大小写转换函数现在几乎没用了,但我认为它不是以英语为中心,而是以计算机语言为中心。由于极端情况和非 ASCII 字符的存在,这些函数现在对英语甚至不是很有用,但它们对于处理丑陋的不区分大小写的 ASCII 文本格式(如 HTML)仍然非常有用。
  • @ruslik - 一切都很好,但考虑一下 OP 实际上在做什么:遍历字符串并检查每个字符。这项任务不会改变。因此,如果您能告诉我如何在混音中添加strlen() 会以某种方式使其更快——除了“我听说它已优化”——我会给你一颗金星。

标签: c string optimization case-sensitive


【解决方案1】:

我的最爱:

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

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

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

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

  1. 加载*c 并对其进行零或符号扩展以填充int 大小的字(取决于普通char 是有符号还是无符号)。
  2. 使用无符号(非溢出捕获)sub 指令减去常量 26。
  3. 如果未设置进位标志,则有条件地跳过其余​​代码。
  4. 否则,将 *c 的值加 32。

步骤 2 和 3 可以在使用比较跳转操作而不是标志的架构上结合使用。我可以看到任何显着幕后成本突然出现的唯一方法是,如果机器不能直接处理字符,或者如果它使用讨厌的(符号/大小或补码)有符号值表示,在这种情况下转换为无符号将是不平凡的。据我所知,没有任何现代嵌入式架构存在这些问题;它们大多与旧式大型机隔离(在较小程度上与 DSP 隔离)。

如果有人担心错误的编译器实际上会为 &lt;&lt;5 执行算术运算,您可以尝试:

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

而不是我的代码。无论如何,这可能更干净,但我通常喜欢避免使用语句,这样我就可以将代码放入循环条件或类似函数的宏中。

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

*c += (64U-*c &amp; *c-91U)&gt;&gt;(CHAR_BIT*sizeof(unsigned)-5);

*c += (64U-*c &amp; *c-91U) &gt;&gt; CHAR_BIT*sizeof(unsigned)-1 &lt;&lt; 5;

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

【讨论】:

  • 请注意,虽然您在代码中没有明确的分支,但许多编译器会由于 '
  • 'Plus 1' 虽然使用 '|32' 允许对切换大小写进行单范围检查,但值得称赞。如果我写了一个无分支切换变体,我肯定会偷走这个想法。
  • @Adisak:正如我所说,我的版本涉及一个分支。您也可以使用一些花哨的位移来消除它,因为您知道 *c 可能具有的值的可能范围,但我认为在大多数平台上它会更昂贵。
  • @R:我从事电子游戏工作。我们工作的平台有严重的分支误判处罚。事实上,即使是像 ATOM 这样的 CPU 也会有严重的分支错误预测惩罚。单个错误预测可能会花费与转换多个字符而不发生错误预测相同的时间。此外,为了执行 SIMD/SWAR(一次处理多个字符以获得最快的结果),您需要消除分支。
  • @R:此外,即使没有 SIMD/SWAR 方法,无分支版本也可以展开和交错以隐藏延迟,从而获得更快的版本。
【解决方案2】:

您的宏至少存在两个主要问题。考虑一下如果我这样称呼他们中的一个会发生什么

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 值(或将其缩小为仅合法值并进行参数检查),但为了说明,这样做可以。)

【讨论】:

  • @Peter Torok-- 啊,我忘记了查找表的想法。那会很快。此外,我将修改我的代码和“注入”错误案例,就像你提到的那样,为了实验起见。感谢cmets。它们都非常有用。
  • 对于嵌入式系统查找表可能太大。
  • @aaa,刚刚在您发表评论时添加了一条注释 :-)
  • 看看我拿 'char slot[] = { 0, 31, 63, 63 }; *c = 插槽[*c/32] + *c%32;'。它几乎可以工作:-)
  • @RLH - 查找表通常是一个不错的选择。但是,它们确实占用空间,特别是在处理器缓存中(在嵌入式系统中可能非常小),并且需要索引取消引用。如果您知道您的字符串主要是字符,那么在常见情况下不分支的if 实际上可能会更快......这是另一种说法“除非您进行分析,否则不要优化。”
【解决方案3】:

注意:问题标题已编辑 - 原始标题是关于优化“请批评 - 用于在 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 之前,上述编译的代码也会非常快。

总而言之,这应该是最快方法的原因:

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

【讨论】:

  • 您能描述一下您是如何编写无分支算法的吗?你能通过对需要分支的朴素算法应用一些启发式和转换来生成无分支算法吗?
  • @Jeremy:嗯,这是一个在两个值之间进行选择的简单案例。它只是找到为选择两个值之一的条件生成二进制掩码的标准,然后使用掩码选择值。这是一个非常简单的方法。大多数时候,可以并行应用相同的算法或进行非常简单的修改的算法。我没有展示,但我展示的展开示例也相当简单。
  • 非常令人印象深刻,我希望看到 SIMD 实现。我从没想过没有树枝的礼帽是可能的。
【解决方案4】:

我不愿回答这个问题,因为我使用小型设备已有 20 多年了。但是,我认为规则几乎相同(可能添加一个):

  1. 尽量减少内存访问
  2. 最小化 CPU 周期
  3. 最小化代码大小

当我开发低级代码时,规则 #1 盖过了所有其他规则。没有任何板载缓存,内存相对于处理器慢得令人难以置信;这就是 C 中存在“寄存器”存储类的原因。今天情况发生了一些变化,但它仍然是最重要的两个问题之一。正如我在一篇文章中评论的那样,查找表是一个好主意,但要认识到这意味着每次测试都需要额外的内存访问。一旦它进入缓存,这可能不是问题,但每次进入函数时,你都要付出多次缓存命中的代价(除非你经常调用它,以至于查找表可以保留在缓存中) .

第 2 条规则看起来像是“呃,你当然想这样做,为什么不是第 1 条规则?”但推理实际上更深入。事实上,在某些方面,这是对规则 #1 的重述,因为每条指令都必须先从内存中取出,然后才能执行。有一个微妙的权衡:在仅整数处理器上,使用查找表来计算三角函数是一个明显的胜利;在带有嵌入式浮点的芯片上,也许不是。

我不确定第 3 条规则是否仍然适用。根据我的经验,总是会争先恐后地削减代码,将众所周知的 20 磅装进 10 磅的麻袋中。但今天最小的麻袋似乎是 50 磅。然而,即使有一个 50 磅的袋子(或数兆字节的 ROM)来保存您的代码/数据,您仍然需要将其拉入缓存(如果有的话)。

新规则 #1:保持管道满载

现代处理器有很深的指令流水线(如果您不熟悉这个术语,请参阅这篇文章:http://arstechnica.com/old/content/2004/09/pipelining-1.ars/1)。深管道的一般经验法则是分支——“if”测试——代价高昂,因为这意味着可能必须刷新管道才能加载新代码。因此,您编写代码以在不太可能的情况下进行分支(请参阅 Adisak 的帖子,了解可能合理的无分支实现;如果可以,请 +1)。

比我有更近期经验的人可能会评论说“现代处理器使用两个分支加载管道,因此没有成本损失。”这一切都很好,但它提出了一个总体规则:

规则 0:优化取决于您的架构和工作负载

洗碗机内的微处理器可能没有管道,也可能没有缓存。当然,它也可能不会做很多文本处理。或者也许两者兼而有之;市场上似乎只有少数主要的嵌入式处理器,因此该电路板上可能有奔腾而不是 8051 衍生产品。即便如此,即使在基于 Pentium 的嵌入式处理器 (http://en.wikipedia.org/wiki/List_of_Intel_Pentium_microprocessors#Embedded_processors) 中也有广泛的范围。对一个人最好的东西可能对另一个人不是最好的。

然后是您正在处理哪种数据的问题。如果您正在处理文本,那么您的大部分数据很可能(但不能保证)是字母,而不是数字或标点符号;所以你可以优化它。

但是,还有更多:我评论了“仅 ASCII,嗯?”在 OP 上;另一位评论者更明确:如果您在 2010 年处理文本,您可能没有处理 ASCII。至少,您将处理 ISO-8859-1 或类似的 8 位字符集。在这种情况下,也许无分支或智能分支(注意管道)解决方案仍然比查找表更快(是的,这是我的假设)。但是,如果您正在处理 Unicode BMP(16 位),则几乎必须使用该表,无论其在内存方面的成本如何,因为没有简单的规则来确定什么是小写字母和大写字母。而且,如果您正在处理 Unicode 的更高层面......好吧,“Old Italics”的大写可能并不那么重要(特别是因为它没有大小写)。

最终,唯一确定的方法是分析给定的实际工作负载。

最后:清除代码 FTW

这篇文章开始于我向 OP 写评论说他/她使用宏是一个坏主意(并且由于 SO 进入维护模式而无法输入)。 Peter Torok(抱歉,我不支持 Unicode 甚至 ISO-8859-1)给出了一个原因,但还有另一个原因:它们是黑盒子。

OP 看起来漂亮而干净:代码短,大量使用位和三元运算符,如果您了解该语言,则易于理解。但是,如果您以扩展形式看到 A_Z,那么理解实际工作会容易得多。这可能会让你想到你做了多少分支,特别是在 ToggleCase 方法中。然后您可能已经考虑过如何重新​​安排这些分支以最小化您正在执行的实际测试的数量。并且也许考虑过维护管道。

【讨论】:

    【解决方案5】:

    好的,到此为止。在这个标签上写...在另一个标签上滚动你的代码:-)

    标题

    1. #define a_z(c) (c &gt;= 'a' &amp;&amp; c &lt;= 'z')

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

    源代码

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

    就是这样。

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

    【讨论】:

    • (('a' &lt;= (c)) &amp;&amp; ((c) &lt;= 'z')) 表明人们对无符号算术的使用一无所知。 ((unsigned)(c)-'a'&lt;='z'-'a') 是写这个的规范方式,它避免了多次评估参数。我讨厌强制转换运算符,所以如果我知道(c)-'a' 不能溢出,我通常将其写为((c)-'a'&lt;26U),否则((c)-65U&lt;26)
    • +1 表示讨厌演员表...感谢您提供有关未签名技巧的课程
    • “三元运算符不会让你的代码更快”——非常正确。常量初始化之类的东西需要它,但如果您在非固有类型上使用它,它实际上会减慢您的代码速度。这是因为 ?: 可以调用类型提升、复制构造和赋值构造,以及生成临时变量来评估非固有类型的表达式。
    【解决方案6】:

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

    ANSI C 在标准库中包含必要的函数,并且可能它们已由编译器供应商针对您的体系结构进行了大量优化。

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

    【讨论】:

      【解决方案7】:

      首先我会说将a_zA_Z 重命名为is_ASCII_Lowercaseis_ASCII_Uppercase。它不像 C 语言,但更容易理解。

      此外,^=?: 的使用也有效,但我发现它的可读性不如简单的 if-statement。

      【讨论】:

        【解决方案8】:

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

        【讨论】:

        • 他的宏确实存在问题,因为它们不止一次地重复他们的论点。在他的代码中使用它是可以的,但通常它很糟糕(除了一些有时可以用来使其正常的编译器扩展之外)。使用好的编译器内联函数会很好,但使用不好的编译器宏会生成更好的代码(尽管原始代码远非最佳)。
        • 你可以用我的版本制作宏,注意不要多次评估他们的论点。看我的回答。
        【解决方案9】:

        怎么样(几乎可以):

        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;
        

        或其他任何...

        【讨论】:

          【解决方案10】:

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

          根据您的系统和您的 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 呢?)。第四,它假设空终止(一些字符串在字符串的开头有一些字符 - 我认为是帕斯卡)。第五,代码大写/小写并不是 100% 天真的显而易见的。另请注意,ENUM 是一个隐藏得很差的整数。你可以通过ToggleCase("some string", 1024)它会编译。

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

          【讨论】:

            【解决方案11】:

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

            效率更高吗?您对代码大小有什么要求? (对于生成的可执行代码,而不是 C 源代码。)在现代桌面系统上,这很少是问题,速度更重要。但是除了“嵌入式系统应用程序”之外,您还没有向我们提供任何详细信息,因此我们无法为您回答这个问题。但是,这不是问题,因为宏中的代码确实如此之小——但你不能假设避免函数调用总是更有效!

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

            简而言之,您必须始终了解自己的确切要求才能确定最佳方案。然后你必须test to verify your performance predictions

            【讨论】:

              【解决方案12】:

              如果尝试一次处理多个字节,我认为最好的方法是强制所有值为 0..127,加上 5 或 37(这将使 'z' 到 'Z' 变为 127) ,记下那个值,然后加26,记下那个值,然后做一些修改。比如:

              unsigned long long orig,t1,t2,result; t1 = (orig & 0x7F7F7F7F7F7F7F7F) + 0x0505050505050505; t2 = t1 + 0x1A1A1A1A1A1A1A1A; 结果 = orig ^ ((~(orig | t1) & t2 & 0x8080808080808080) >> 2);

              嗯...我想这很有效,即使适用于 32 位机器。如果四个寄存器预加载了适当的常数,ARM 可以使用最佳代码执行七条指令执行七个周期的操作;我怀疑编译器会发现优化(或发现将常量保存在寄存器中会有所帮助——如果常量不保存在寄存器中,单独处理字节会更快)。

              【讨论】:

              • 您可以屏蔽高位并将其用作直通选择,然后它将适用于 0-255 的所有值。但是如果你知道你的 ASCII 是 0-127,你可以跳过额外的工作:-)
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-07-30
              • 2021-05-03
              • 1970-01-01
              相关资源
              最近更新 更多