如果在循环条件下使用,strlen是否会被多次计算?


109

我不确定以下代码是否会导致冗余计算,或者它是否特定于编译器?

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

strlen()每次i增加时都会计算?


14
我要猜测的是,如果没有能够检测到“ ss”的永不改变的复杂优化,则肯定会在循环中发生变化。最好进行编译,并查看程序集以查看。
MerickOWA 2012年

6
这取决于编译器,优化级别以及您(可能)ss对循环内部执行的操作。
赫里斯托·伊利耶夫

4
如果编译器可以证明它ss永远不会被修改,则它可以将计算提升到循环之外。
丹尼尔·菲舍尔

10
@Mike:“需要对strlen所做的事情进行编译时分析”-strlen可能是一个内在函数,在这种情况下,优化器知道其工作内容。
史蒂夫·杰索普

3
@MikeSeymour:也许没有,也许没有。strlen由C语言标准定义,其名称保留给该语言定义,以供程序使用,因此程序不能随意提供其他定义。编译器和优化器有权假定strlen仅取决于其输入,而不修改其输入或任何全局状态。优化的挑战在于确定ss指向的内存不会被循环内的任何代码更改。对于当前的编译器,这完全可行,具体取决于特定的代码。
埃里克·波斯特皮希尔

Answers:


138

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

我会做类似的事情

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

或可能

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

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


14
如果您知道您没有在操纵字符串,那么第二个就更可取了,因为从本质上讲这是strlen无论如何都要执行的循环。
mlibby 2012年

26
@alk:如果字符串可能变短,那么这两个都是错误的。
Mike Seymour

3
@alk:如果您要更改字符串,则for循环可能不是迭代每个字符的最佳方法。我认为while循环更直接,更容易管理索引计数器。
mlibby 2012年

2
理想的情况包括在Linux 使用GCC进行编译,其中strlen标记为__attribute__((pure))允许编译器取消多个调用。GCC属性
大卫·罗德里格斯- dribeas

6
第二个版本是理想且最惯用的形式。它允许您仅传递一次字符串而不是传递两次,这对于长字符串将具有更好的性能(尤其是缓存一致性)。
R .. GitHub停止帮助ICE

14

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

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

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

有关更多信息,请参见此链接

在下面的代码中,每次循环运行strlen都将计算整个字符串的长度,这将降低效率,花费更多时间并占用更多内存。

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

3
我可以同意“ [效率更高]”,但是使用更少的内存?我唯一能想到的内存使用差异将是在调用过程中的调用堆栈中strlen,如果运行得太紧,您可能还应该考虑取消其他一些函数调用……
CVn

@MichaelKjörling好吧,如果您使用“ strlen”,则在循环中,每次循环运行时它都必须扫描整个字符串,而在上面的代码中,“ str [ix]”,它仅在循环的每个周期中扫描一个元素。循环的位置由“ ix”表示。因此,它比“ strlen”占用更少的内存。
codeDEXTER 2012年

1
实际上,我不确定这是否有意义。一个非常简单的strlen实现将类似于int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; }您在答案中的代码中所做的事情。我不是在争论一次而不是两次遍历字符串会节省更多时间,但是我看不到一个或另一个使用更多或更少的内存。还是您指的是用于保存字符串长度的变量?
CVn 2012年

@MichaelKjörling请参见上面编辑的代码和链接。至于内存,每次循环运行时,每个迭代的值都存储在内存中,如果为“ strlen”,则它一次又一次地对整个字符串进行计数,因此需要更多的内存来存储。并且还因为与Java不同,C ++没有“垃圾收集器”。那我也可能是错的。请参阅有关C ++中缺少“垃圾收集器”的链接
codeDEXTER 2012年

1
@ aashis2s缺少垃圾收集器仅在堆上创建对象时起作用。范围和结束时,堆栈上的对象将被销毁。
伊克,2012年

9

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

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

例如,如果ssfor循环中使用只读函数,但未将ss-parameter 声明为const,则编译器甚至无法知道ss循环中未更改该参数,因此必须strlen(ss)在每次迭代中进行计算。


3
+1:不仅ss不能在for循环中更改;不能从循环中调用的任何函数访问和更改它(要么因为它作为参数传递,要么因为它是全局变量或文件作用域变量)。常量资格也可能是一个因素。
乔纳森·勒夫勒

4
我认为编译器几乎不可能知道'ss'不会改变。可能存在指向“ ss”内部存储器的杂散指针,编译器不知道这会改变“ ss”
MerickOWA 2012年

Jonathan是对的,本地const字符串可能是确保编译器无法更改“ s”的唯一方法。
MerickOWA 2012年

2
@MerickOWA:确实,这restrict就是C99中要做的事情之一。
史蒂夫·杰索普

4
关于最后一个参数:如果您ss在for循环中调用了一个只读函数,那么即使声明const char*了其参数,编译器仍然需要重新计算长度,除非(a)知道ss指向const对象, (b)可以内联该函数,否则可以看到它是只读的。接受const char*参数并不保证不会修改所指向的数据,因为只要char*修改后的对象不是const并且不是字符串文字,就可以强制转换并进行修改。
史蒂夫·杰索普

4

如果ss是类型,const char *并且您没有放弃const循环内的本质,则编译器可能只会调用strlen,那么如果打开了优化功能,一次。但这当然不是可以指望的行为。

您应该将strlen结果保存在变量中,然后在循环中使用此变量。如果您不想创建其他变量,具体取决于您正在执行的操作,则可能不希望通过反转循环来向后迭代。

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

1
打电话strlen完全是错误的。循环播放直到结束。
R .. GitHub停止帮助ICE

i > 0?那不应该i >= 0在这里吗?就我个人而言,strlen(s) - 1如果从字符串上向后迭代,那么终止也\0无需特别考虑。
CVn 2012年

2
@MichaelKjörling i >= 0仅在您初始化为时才起作用strlen(s) - 1,但是如果您有一个长度为零的字符串,则初始值会下溢
Praetorian

@Prætorian,零长度字符串上的好点。我写评论时没有考虑这种情况。C ++是否i > 0在初始循环进入时对表达式求值?如果不是这样,那么您是对的,零长度的情况肯定会打破循环。如果是的话,您“简单地”得到一个有符号的i== -1 <0,因此如果条件为,则没有循环项i >= 0
CVn 2012年

@MichaelKjörling是的,在第一次执行循环之前先评估退出条件。strlen的返回类型是无符号的,因此(strlen(s)-1) >= 0对于零长度字符串,其求值为true。
Praetorian

3

正式是 strlen()预计将在每次迭代中调用。

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


3

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

  1. 功能 strlen无副作用
  2. 指向的内存ss在循环期间不会更改

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


好吧,它可以通过静态分析了解这些内容,但是我想您的意思是,当前没有在任何C ++编译器中实现这种分析。
GManNickG

@GManNickG它肯定可以证明#1,但是#2很难。对于单线程,可以肯定地证明这一点,但对于多线程环境则不能。
JaredPar

1
也许我很固执,但我认为在多线程环境中也可以实现第二个目标,但是绝对不能没有强大的推理系统。只是在这里沉思。绝对超出了任何当前C ++编译器的范围。
GManNickG

@GManNickG我不认为这是可能的,尽管在C / C ++中。我可以很容易地将地址隐藏ss为a size_t或将其分配到多个byte值中。然后,我弯曲的线程可以只将字节写入该地址,并且编译器将具有理解与之相关的方式ss
JaredPar

1
@JaredPar:很抱歉,您可能声称int a = 0; do_something(); printf("%d",a);无法优化,do_something()可能会导致您执行未初始化的int事情,或者可能会爬回堆栈并a故意修改。实际上,gcc 4.5确实do_something(); printf("%d",0);使用-O3 对其进行了优化
Steve Jessop

2

是的。我增加时都会计算strlen。

如果您没有在循环中更改ss with ,则不会影响逻辑,否则会影响逻辑

使用以下代码更安全。

int length = strlen(ss);

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


2

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

如果您想提高效率,那么请务必记住将所有内容保存在局部变量中……这会花费一些时间,但是非常有用。

您可以使用如下代码:

String str="ss";
int l = strlen(str);

for ( int i = 0; i < l ; i++ )
{
    // blablabla
}


2

现在不常见,但是20年前在16位平台上,我建议这样做:

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

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


1

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

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

1

哎呀,即使在理想情况下,也该死!

截至今天(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() 该循环的每一个迭代。惊人。

这也意味着@Praetorian和@JaredPar的典故/如意算盘不会成功。


0

是的,简单来说。而且,在很少见的情况下,编译器希望发现编译器根本不做任何更改,这是优化步骤ss。但是在安全条件下,您应该认为是。在某些情况下,例如multithreaded事件驱动程序中的in ,如果您将其视为“否”,则可能会出现错误。请放心,因为它不会过多地提高程序复杂性。


0

是。

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

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

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

我认为进行特殊修改永远不会改变ss的长度,仅改变其内容,因此(一个非常聪明的编译器)仍然可以优化strlen
达伦·库克

0

我们可以轻松地对其进行测试:

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

循环条件在每次重复之后评估,然后重新启动循环。

另外,请注意用于处理字符串长度的类型。应该是在stdio中size_t定义的unsigned int。比较并强制转换int可能会导致一些严重的漏洞问题。


0

好吧,我注意到有人说默认情况下,任何“聪明”的现代编译器都会对其进行优化。顺便看看没有优化的结果。我试过:
最小的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但是,如果字符串的地址具有限定符,则足以证明优化是合理的,而不必考虑其他任何条件。
超级猫

0

详细阐述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 > 0 因为与0的比较比与任何其他数字的比较(略)快。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.