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


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

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

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


108
2017-07-06 15:18


起源


我猜想如果没有复杂的优化可以检测到循环中的'ss'永远不变,那么是的。最好编译,看看程序集看看。 - MerickOWA
它取决于编译器,优化级别以及您(可能)的作用 ss 在循环内。 - Hristo Iliev
如果编译器可以证明这一点 ss 永远不会被修改,它可以将计算提升出循环。 - Daniel Fischer
@Mike:“需要对strlen做什么进行编译时分析” - strlen可能是一个内在的,在这种情况下,优化器知道它的作用。 - Steve Jessop
@MikeSeymour:没有,也许没有。 strlen由C语言标准定义,其名称保留用于语言定义的用法,因此程序不能自由提供不同的定义。编译器和优化器有权假设strlen完全依赖于它的输入,并且不会修改它或任何全局状态。这里优化的挑战是确定ss指向的内存不会被循环内的任何代码改变。对于当前的编译器,这完全可行,具体取决于具体的代码。 - Eric Postpischil


答案:


是, strlen() 将在每次迭代时进行评估。在理想的情况下,优化者可能会推断出价值不会改变,但我个人不会依赖于此。

我会做类似的事情

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

或者可能

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

只要字符串在迭代期间不会改变长度。如果可能,那么你需要打电话 strlen() 每一次,或通过更复杂的逻辑来处理它。


135
2017-07-06 15:24



如果你知道你没有操纵字符串,那么第二个就更好了,因为那基本上就是你要执行的循环 strlen 无论如何。 - mlibby
@alk:如果字符串可能缩短,那么这两个都是错误的。 - Mike Seymour
@mcl:当然,如果...... ;-) - alk
@alk:如果你要更改字符串,for循环可能不是迭代每个字符的最佳方法。我认为while循环更直接,更容易管理索引计数器。 - mlibby
第二个版本是理想和最惯用的形式。它允许您只传递一次字符串而不是两次,这对于长字符串将具有更好的性能(尤其是缓存一致性)。 - R..


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

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;
}

14
2017-07-06 15:31



我同意“[它]效率更高”,但使用更少的内存?我能想到的唯一内存使用差异是在调用堆栈期间 strlen 打电话,如果你跑得那么紧,你可能应该考虑去除其他一些函数调用...... - Michael Kjörling
@MichaelKjörling好吧如果你使用“strlen”,那么在循环中它必须在每次循环运行时扫描整个字符串,而在上面的代码中“str [ix]”,它只扫描一个元素在每个循环中循环,其位置由“ix”表示。因此它比“strlen”占用更少的内存。 - codeDEXTER
实际上,我不确定这有多大意义。一个非常天真的strlen实现就像是 int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; } 这正是你在答案中代码中正在做的事情。我不是在争论迭代字符串而不是两次更多 时间 - 效率,但我没有看到一个或另一个使用更多或更少的内存。或者您指的是用于保存字符串长度的变量? - Michael Kjörling
@MichaelKjörling请参阅上面编辑的代码和链接。至于内存 - 每次循环运行时,每个迭代的值都存储在内存中,而在'strlen'的情况下,因为它一次又一次地计算整个字符串,需要更多的内存来存储。而且因为与Java不同,C ++没有“垃圾收集器”。那我也错了。看到 链接 关于在C ++中没有“垃圾收集器”的问题。 - codeDEXTER
@ aashis2s缺少垃圾收集器只在堆上创建对象时起作用。一旦范围结束,堆栈上的对象就会被销毁。 - Ikke


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

除此之外,编译器必须知道 strlen(ss) 不会改变。只有这样才有效 ss 没有改变 for 循环。

例如,如果您使用只读函数 ss 在 for 循环,但不要声明 ss - 参数为 const,编译器甚至不知道 ss 在循环中没有改变,必须计算 strlen(ss) 在每次迭代中。


9
2017-07-06 15:23



+1:不仅必须 ss 不要改变 for 循环;它不能被循环中调用的任何函数访问和更改(因为它作为参数传递,或者因为它是全局变量或文件范围变量)。限定资格也可能是一个因素。 - Jonathan Leffler
我认为编译器不太可能知道“ss”不会改变。可能存在指向“ss”内存的杂散指针,编译器不知道这可能会改变“ss” - MerickOWA
Jonathan是对的,一个本地const字符串可能是编译器确保没有办法让'ss'改变的唯一方法。 - MerickOWA
@MerickOWA:的确,那就是其中之一 restrict 适用于C99。 - Steve Jessop
关于你的最后一段:如果你打电话给一个只读功能 ss 在for循环中,即使声明了它的参数 const char*,编译器 仍然 需要重新计算长度,除非(a)它知道 ss 指向const对象,而不是仅仅是指向const的指针,或者(b)它可以内联函数或以其他方式看到它是只读的。拿一个 const char* 参数是 不 承诺不修改指向的数据,因为它有效转换为 char* 如果修改的对象不是const且不是字符串文字,则修改。 - Steve Jessop


如果 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
}

4
2017-07-06 15:29



打电话是个错误 strlen 一点都不只需循环直到你结束。 - R..
i > 0?如果不是这样的话 i >= 0 这里?就个人而言,我也会从 strlen(s) - 1 如果向后迭代字符串,则终止 \0 不需要特别考虑。 - Michael Kjörling
@MichaelKjörling i >= 0 仅在初始化为时才有效 strlen(s) - 1,但是如果你有一个长度为零的字符串,则初始值下溢 - Praetorian
@Prætorian,关于零长度字符串的好点。当我写评论时,我没有考虑这种情况。 C ++会评估 i > 0 表达式初始循环条目?如果没有,那么你是对的,零长度的情况肯定会打破循环。如果确实如此,您“只是”获得签名 i== -1 <0所以如果条件是,则没有循环条目 i >= 0。 - Michael Kjörling
@MichaelKjörling是的,在第一次执行循环之前评估退出条件。 strlen返回类型是无符号的,所以 (strlen(s)-1) >= 0 对于零长度字符串,求值为true。 - Praetorian


正式是的, strlen() 预计每次迭代都会被调用。

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


3
2017-07-06 15:21





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

  1. 功能 strlen 副作用是免费的
  2. 记忆指向的 ss 在循环的持续时间内不会改变

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


3
2017-07-06 15:22



好吧 可以 通过静态分析知道这些事情,但我认为你的观点是这种分析目前还没有在任何C ++编译器中实现,是吗? - GManNickG
@GManNickG肯定能证明#1,但#2更难。对于单个线程是的它肯定可以证明它,但不适用于多线程环境。 - JaredPar
也许我很顽固,但我认为在多线程环境中也有可能实现第二,但绝对不是没有强大的推理系统。只是在这里思考;绝对超出了当前任何C ++编译器的范围。 - GManNickG
@GManNickG我不认为它在C / C ++中是可能的。我可以很容易地隐藏地址 ss 变成一个 size_t 或者将它分成几个 byte 值。然后我的狡猾的线程可以只将字节写入该地址,编译器就会知道它与之相关的理解方式 ss。 - JaredPar
@JaredPar:很抱歉,你可以声称 int a = 0; do_something(); printf("%d",a); 在此基础上无法优化 do_something() 可以做你未初始化的int事情,或者可以爬回堆栈并修改 a 故意。事实上,gcc 4.5确实优化了它 do_something(); printf("%d",0); 与-O3 - Steve Jessop


是的 strlen(ss) 将计算每次迭代的长度。如果你正在增加 ss 从某种程度上也增加了 i;会有无限循环。


2
2017-07-07 11:04





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

如果你想提高效率,那么一定要记住将所有内容保存在局部变量中...这需要时间,但它非常有用..

您可以使用以下代码:

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

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

2
2017-07-11 09:06





是, strlen(ss) 将在每次代码运行时计算。


2
2017-07-06 22:03





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

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

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


2
2017-07-11 13:24