【问题标题】:Will strlen be calculated multiple times if used in a loop condition?如果在循环条件中使用 strlen 会被计算多次吗?
【发布时间】:2012-07-07 01:55:07
【问题描述】:

我不确定下面的代码是否会导致冗余计算,还是编译器特有的?

for (int i = 0; i < strlen(ss); ++i)
{
    // blabla
}

strlen() 每增加一次strlen() 都会计算吗?

【问题讨论】:

  • 我猜如果没有复杂的优化可以检测到'ss'在循环中永远不会改变,那么是的。最好编译一下,看看汇编看看。
  • 这取决于编译器、优化级别以及您(可能)在循环内对ss 执行的操作。
  • 如果编译器可以证明ss 从未被修改过,它可以将计算提升到循环之外。
  • @Mike:“需要在编译时分析 strlen 到底做了什么” - strlen 可能是一个内在函数,在这种情况下优化器知道它做了什么。
  • @MikeSeymour:没有可能,可能没有。 strlen 是由 C 语言标准定义的,它的名称是为该语言定义的用途而保留的,因此程序不能随意提供不同的定义。编译器和优化器有权假定 strlen 仅依赖于其输入并且不修改它或任何全局状态。这里优化的挑战是确定 ss 指向的内存不会被循环内的任何代码更改。这对于当前的编译器来说是完全可行的,具体取决于具体的代码。

标签: c++ c gcc optimization strlen


【解决方案1】:

是的,strlen() 将在每次迭代中进行评估。在理想情况下,优化器可能能够推断出该值不会改变,但我个人不会依赖这一点。

我会做类似的事情

for (int i = 0, n = strlen(ss); i < n; ++i)

或者可能

for (int i = 0; ss[i]; ++i)

只要字符串在迭代期间不会改变长度。如果可能,那么您需要每次调用strlen(),或者通过更复杂的逻辑处理。

【讨论】:

  • 如果你知道你没有在操作字符串,那么第二个更可取,因为这基本上是strlen 将执行的循环。
  • @alk:如果字符串可能被缩短,那么这两个都是错误的。
  • @alk:如果您要更改字符串,for 循环可能不是遍历每个字符的最佳方式。我认为while循环更直接,更容易管理索引计数器。
  • 理想情况包括在linux下使用GCC编译,其中strlen被标记为__attribute__((pure)),允许编译器省略多个调用。 GCC Attributes
  • 第二个版本是最理想也是最惯用的形式。它允许您只传递字符串一次而不是两次,这对于长字符串会有更好的性能(尤其是缓存一致性)。
【解决方案2】:

是的,每次使用循环。然后它每次都会计算字符串的长度。 所以像这样使用它:

char str[30];
for ( int i = 0; str[i] != '\0'; i++)
{
//Something;
}

在上面的代码中,str[i] 每次循环开始一个循环时,只验证字符串中位置i 的一个特定字符,因此它占用的内存更少,效率更高。

请参阅此Link 了解更多信息。

在下面的代码中,每次循环运行strlen 都会计算整个字符串的长度,效率较低,需要更多时间和更多内存。

char str[];
for ( int i = 0; i < strlen(str); i++)
{
//Something;
}

【讨论】:

  • 我可以同意“[它]更高效”,但使用更少的内存?我能想到的唯一内存使用差异是在 strlen 调用期间的调用堆栈中,如果你运行得那么紧,你可能应该考虑省略其他一些函数调用......跨度>
  • @MichaelKjörling 好吧,如果您使用“strlen”,那么在循环中它必须在每次循环运行时扫描整个字符串,而在上面的代码中“str[ix]”,它只在循环的每个循环中扫描一个元素,其位置由“ix”表示。因此它比“strlen”占用更少的内存。
  • 我不确定这是否有意义,实际上。 strlen 的一个非常幼稚的实现将类似于int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; },这几乎正是您在答案中的代码中所做的。我并不是说迭代字符串一次而不是两次更时间-效率更高,但我没有看到其中一个或另一个使用或多或少的内存。还是你指的是用来保存字符串长度的变量?
  • @MichaelKjörling 请查看上面的编辑代码和链接。至于内存 - 每次循环运行时,每个迭代的值都存储在内存中,如果是“strlen”,因为它一次又一次地计算整个字符串,它需要更多的内存来存储。也因为与 Java 不同,C++ 没有“垃圾收集器”。那我也可能是错的。请参阅 link 关于 C++ 中缺少“垃圾收集器”。
  • @aashis2s 缺少垃圾收集器仅在在堆上创建对象时起作用。一旦作用域结束,堆栈上的对象就会被销毁。
【解决方案3】:

一个好的编译器可能不会每次都计算它,但我认为你不能确定每个编译器都会这样做。

除此之外,编译器必须知道strlen(ss) 不会改变。仅当 ssfor 循环中未更改时才适用。

例如,如果您在for 循环中对ss 使用只读函数,但没有将ss 参数声明为const,编译器甚至无法知道ss 不是在循环中更改并且必须在每次迭代中计算strlen(ss)

【讨论】:

  • +1:不仅ss不能在for循环中改变;它不能被循环中调用的任何函数访问和更改(因为它作为参数传递,或者因为它是全局变量或文件范围变量)。 const 资格也可能是一个因素。
  • 我认为编译器不太可能知道'ss'不会改变。可能存在指向“ss”内部内存的杂散指针,编译器不知道这可能会改变“ss”
  • Jonathan 是对的,本地 const 字符串可能是编译器确保 'ss' 无法更改的唯一方法。
  • @MerickOWA:确实,这是restrict 在 C99 中的用途之一。
  • 关于你的最后一段:如果你在for循环中对ss调用了一个只读函数,那么即使它的参数声明为const char*,编译器仍然 i> 需要重新计算长度,除非 (a) 它知道 ss 指向一个 const 对象,而不仅仅是一个指向 const 的指针,或者 (b) 它可以内联函数或以其他方式看到它是只读的。采用const char* 参数not 承诺不修改指向的数据,因为如果修改的对象不是 const 并且不是字符串文字。
【解决方案4】:

如果 ssconst char * 类型,并且您没有在循环中丢弃 constness,如果启用了优化,编译器可能只会调用一次 strlen。但这当然不是可以指望的行为。

您应该将strlen 结果保存在一个变量中,并在循环中使用该变量。如果您不想创建额外的变量,则取决于您正在做什么,您可能会通过反转循环来向后迭代。

for( auto i = strlen(s); i > 0; --i ) {
  // do whatever
  // remember value of s[strlen(s)] is the terminating NULL character
}

【讨论】:

  • 打电话给strlen是个错误。循环直到你到达终点。
  • i &gt; 0?这不应该是i &gt;= 0吗?就个人而言,如果向后迭代字符串,我也会从 strlen(s) - 1 开始,那么终止 \0 不需要特别考虑。
  • @MichaelKjörling i &gt;= 0 仅在您初始化为 strlen(s) - 1 时才有效,但如果您的字符串长度为零,则初始值下溢
  • @Prætorian,关于零长度字符串的要点。我写评论时没有考虑这种情况。 C++ 是否在初始循环条目上评估 i &gt; 0 表达式?如果没有,那么你是对的,零长度的情况肯定会打破循环。如果是这样,你“简单地”得到一个签名的i == -1 i >= 0,则没有循环条目。
  • @MichaelKjörling 是的,在第一次执行循环之前评估退出条件。 strlen 的返回类型是无符号的,因此 (strlen(s)-1) &gt;= 0 对于零长度字符串的计算结果为 true。
【解决方案5】:

形式上是的,预计每次迭代都会调用strlen()

无论如何,我不想否定存在一些聪明的编译器优化的可能性,这将优化在第一个之后对 strlen() 的任何连续调用。

【讨论】:

    【解决方案6】:

    整个谓词代码将在for 循环的每次迭代中执行。为了记住strlen(ss) 调用的结果,编译器至少需要知道这一点

    1. strlen 函数没有副作用
    2. ss 指向的内存在循环期间不会改变

    编译器不知道这些事情中的任何一个,因此无法安全地记住第一次调用的结果

    【讨论】:

    • 嗯,它可以通过静态分析知道这些东西,但我认为你的意思是,目前任何 C++ 编译器都没有实现这种分析,是吗?
    • @GManNickG 它绝对可以证明 #1 但 #2 更难。对于单线程是的,它肯定可以证明这一点,但对于多线程环境则不行。
    • 也许我很固执,但我认为在多线程环境中第二个也是可能的,但绝对不能没有非常强大的推理系统。只是在这里沉思;绝对超出任何当前 C++ 编译器的范围。
    • @GManNickG 我认为这在 C / C++ 中是不可能的。我可以很容易地将ss 的地址存储到size_t 中,或者将其分成几个byte 值。然后,我的狡猾线程可以将字节写入该地址,编译器就会知道它与ss 相关的理解方式。
    • @JaredPar:对不起,你可以声称int a = 0; do_something(); printf("%d",a); 无法优化,因为do_something() 可以做你未初始化的 int 事情,或者可以爬回堆栈并修改a 故意的。事实上,gcc 4.5 确实使用 -O3 将其优化为 do_something(); printf("%d",0);
    【解决方案7】:

    是的。每次i增加时都会计算strlen。

    如果你没有用在循环中改变ss意味着它不会影响逻辑否则会影响。

    使用以下代码更安全。

    int length = strlen(ss);
    
    for ( int i = 0; i < length ; ++ i )
    {
     // blabla
    }
    

    【讨论】:

      【解决方案8】:

      是的,strlen(ss) 将在每次迭代时计算长度。如果您以某种方式增加ss 并增加i;会有无限循环。

      【讨论】:

        【解决方案9】:

        是的,strlen() 函数被调用每次循环被评估。

        如果你想提高效率,那么永远记得把所有东西都保存在局部变量中......这需要时间,但它非常有用..

        您可以使用如下代码:

        String str="ss";
        int l = strlen(str);
        
        for ( int i = 0; i < l ; i++ )
        {
            // blablabla
        }
        

        【讨论】:

          【解决方案10】:

          是的,每次代码运行时都会计算strlen(ss)

          【讨论】:

            【解决方案11】:

            现在不常见,但 20 年前在 16 位平台上,我推荐这个:

            for ( char* p = str; *p; p++ ) { /* ... */ }
            

            即使你的编译器在优化方面不是很聪明,上面的代码也可以产生好的汇编代码。

            【讨论】:

              【解决方案12】:

              是的。测试不知道 ss 在循环内没有改变。如果你知道它不会改变,那么我会写:

              int stringLength = strlen (ss); 
              for ( int i = 0; i < stringLength; ++ i ) 
              {
                // blabla 
              } 
              

              【讨论】:

                【解决方案13】:

                啊,即使在理想情况下,它也会该死的!

                截至今天(2018 年 1 月),gcc 7.3 和 clang 5.0,如果您编译:

                #include <string.h>
                
                void bar(char c);
                
                void foo(const char* __restrict__ ss) 
                {
                    for (int i = 0; i < strlen(ss); ++i) 
                    {
                        bar(*ss);
                    }
                }    
                

                所以,我们有:

                • ss 是一个常量指针。
                • ss 标记为 __restrict__
                • 循环体不能以任何方式接触ss 指向的内存(好吧,除非它违反了__restrict__)。

                仍然,两个编译器都执行strlen()every single iteration of that loop。太棒了。

                这也意味着@Praetorian 和@JaredPar 的典故/一厢情愿不会成功。

                【讨论】:

                  【解决方案14】:

                  是的,简单来说。 如果编译器发现在ss 中根本没有进行任何更改,则作为优化步骤,编译器希望在极少数情况下出现很小的“否”。但在安全的情况下,你应该认为它是 YES。在multithreaded 和事件驱动程序中存在一些情况,如果您认为它是“否”,它可能会出现错误。 谨慎行事,因为它不会过多地提高程序的复杂性。

                  【讨论】:

                    【解决方案15】:

                    是的。

                    strlen() 每次在i 增加且未优化时计算。

                    下面的代码显示了为什么编译器不应该优化strlen()

                    for ( int i = 0; i < strlen(ss); ++i )
                    {
                       // Change ss string.
                       ss[i] = 'a'; // Compiler should not optimize strlen().
                    }
                    

                    【讨论】:

                    • 我认为进行特定修改永远不会改变 ss 的长度,只会改变它的内容,所以(一个非常非常聪明的)编译器仍然可以优化 strlen
                    【解决方案16】:

                    我们可以很容易地测试它:

                    char nums[] = "0123456789";
                    size_t end;
                    int i;
                    for( i=0, end=strlen(nums); i<strlen(nums); i++ ) {
                        putchar( nums[i] );
                        num[--end] = 0;
                    }
                    

                    循环条件在每次重复之后、重新开始循环之前进行评估。

                    还要注意用于处理字符串长度的类型。它应该是size_t,它在标准输入输出中被定义为unsigned int。比较并将其转换为 int 可能会导致一些严重的漏洞问题。

                    【讨论】:

                      【解决方案17】:

                      好吧,我注意到有人说它默认由任何“聪明”的现代编译器优化。顺便看看没有优化的结果。我试过了:
                      最少的 C 代码:

                      #include <stdio.h>
                      #include <string.h>
                      
                      int main()
                      {
                       char *s="aaaa";
                      
                       for (int i=0; i<strlen(s);i++)
                        printf ("a");
                       return 0;
                      }
                      

                      我的编译器:g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
                      汇编代码生成命令:g++ -S -masm=intel test.cpp

                      Gotten assembly code at the output:
                          ...
                          L3:
                      mov DWORD PTR [esp], 97
                      call    putchar
                      add DWORD PTR [esp+40], 1
                          .L2:
                           THIS LOOP IS HERE
                          **<b>mov    ebx, DWORD PTR [esp+40]
                      mov eax, DWORD PTR [esp+44]
                      mov DWORD PTR [esp+28], -1
                      mov edx, eax
                      mov eax, 0
                      mov ecx, DWORD PTR [esp+28]
                      mov edi, edx
                      repnz scasb</b>**
                           AS YOU CAN SEE it's done every time
                      mov eax, ecx
                      not eax
                      sub eax, 1
                      cmp ebx, eax
                      setb    al
                      test    al, al
                      jne .L3
                      mov eax, 0
                           .....
                      

                      【讨论】:

                      • 我不愿意相信任何试图优化它的编译器,除非字符串的地址是restrict-qualified。虽然在某些情况下这种优化是合法的,但在没有restrict 的情况下可靠地识别此类情况所需的努力,无论采取任何合理的措施,几乎肯定会超过收益。但是,如果字符串的地址有一个 const restrict 限定符,那么这本身就足以证明优化的合理性,而无需查看其他任何内容。
                      【解决方案18】:

                      详细阐述 Prætorian 的回答,我建议如下:

                      for( auto i = strlen(s)-1; i > 0; --i ) {foo(s[i-1];}
                      
                      • auto 因为您不想关心 strlen 返回的类型。 C++11 编译器(例如 gcc -std=c++0x,不完全是 C++11,但自动类型可以工作)将为您完成。
                      • i = strlen(s) 因为您想与 0 进行比较(见下文)
                      • i &gt; 0 因为与 0 的比较比与任何其他数字的比较(略)快。

                      缺点是您必须使用i-1 才能访问字符串字符。

                      【讨论】:

                        猜你喜欢
                        • 2023-03-19
                        • 2011-01-04
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-07-22
                        • 1970-01-01
                        • 2021-09-09
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多