布尔比较的效率?在C中

3

我正在用C语言编写一个循环,想要优化一下。虽然这里并不是非常关键,因为我只是在练习,但为了更进一步的知识,我想知道:

例如以下代码片段的循环:

int i = 0;
while (i < 10) {
    printf("%d\n", i);
    i++;
}

处理器在每次迭代时都会检查两个条件:(i < 10)(i == 10)吗?还是只检查(i < 10),如果为真,就继续执行?
如果同时检查两个条件,那么不会造成额外的开销吗?
int i = 0;
while (i != 10) {
    printf("%d\n", i);
    i++;
}

想要更高效吗?

谢谢!


如果你在循环中使用"printf",那么布尔比较的成本将会微不足道,并且根本不重要。printf是一项相当昂贵的操作。 - Ira Baxter
1
它几乎肯定没有区别,但真正的问题是:你为什么在意?你是否在程序中识别出需要优化此类操作的性能热点?如果是这样,请发布有关您的性能问题的更多详细信息。 - Paul R
@Ira Baxter:这只是一个例子,但知道这点很好! - Wolfin
3
@Wolfin:可能不需要 - 循环的成本与循环内部的内容相比微不足道,除非你在循环内执行的操作非常简单。另外,你应该了解过早优化的危害,例如:http://en.wikipedia.org/wiki/Program_optimization#When_to_optimize. - Paul R
1
@Wolfin:听起来你应该发布你的统计分析代码,并问如何进行优化。 - Ira Baxter
显示剩余4条评论
9个回答

9

这两个值将会在一条汇编指令中被翻译。大多数CPU都有比较指令,包括小于、小于等于、等于和不等于。


好的,这解决了我的问题。但是它似乎有些不对劲...按位地看,两个比较不能同时进行,对吗?我想这无关紧要。 - Wolfin
当然可以。这些比较涉及到两个标志位。Z标志表示结果为0,N标志表示结果为负数。将这两个标志位组合起来构建所有比较指令的逻辑粘合剂是廉价的。顺便说一下,你有没有注意到,对于所有位,两个64位整数的相加是同时进行的? - mouviciel
@wolfin:实际上,看看mouviciel的回答吧。他的回答比我的好。 - slebetman
1
@Wolfin - 那是一个好问题。答案是,当执行操作时,CPU通常会设置一系列标志位,并且CPU可以查询其中多个标志位来决定是否进行分支。例如,在比较i和10时,编译器通常会执行减法操作,如果一个操作数小于另一个,则会设置进位标志,如果操作数相等,则会设置零标志。通过如果设置了进位或零标志中的任意一个,就可以做出小于或等于的判断,这可以在单个指令中完成。 - Michael Burr
@slebetman:可能在两种情况下都会检查是否等于10,但是在一种情况下使用了“如果负则跳转”,而在另一种情况下使用了“如果负或零则跳转”,就像@Michael Burr所说的那样。 - Vatine
显示剩余3条评论

5

关于这些优化问题的有趣之处在于,它们经常显示出为何应该先编写清晰/正确的代码再考虑这些操作的性能影响(这往往没有任何区别)。

您的两个示例循环的行为不同:

int i = 0;
/* this will print 11 lines (0..10) */
while (i <= 10) {
    printf("%d\n", i);
    i++;
}

同时,

int i = 0;
/* This will print 10 lines (0..9) */
while (i != 10) {
    printf("%d\n", i);
    i++;
}

回答你的问题,几乎可以肯定两种结构的性能是相同的(假设你修复了问题,使循环计数相同)。例如,如果您的处理器只能分别检查相等和一个值是否小于另一个值(这将是非常不寻常的处理器),那么编译器可能会将 (i <= 10) 转换为 (i < 11) 测试 - 或者可能是一个 (i != 11) 测试。

啊,打错字了。已经修复了。 - Wolfin

3
这是早期优化的一个清晰例子......在我看来,新手程序员往往太容易担心这些问题。如果你必须要担心它,那就学会基准测试和剖析你的代码,这样你的担忧就是基于证据而不是推测的。
针对你的具体问题,首先,在我的职业生涯中,任何一个C编译器都没有把“<=”实现为两个单独测试“<”和“==”的操作。包括一些非常愚蠢的编译器。注意,对于整数,“a <= 5”是与“a < 6”相同的条件,如果目标架构要求仅使用“<”,那么代码生成器将执行该操作。
你的第二个问题,即“while(i!=10)”可能更有效率,引发了一个有趣的防御性编程问题。首先,在任何合理的目标架构下,它都不会更有效率。然而,它提高了一个小错误可能导致更大失败的潜在风险。考虑这种情况:如果循环体内的某行代码修改了“i”,比如使其大于10,会发生什么?循环需要多长时间才能结束?是否会有其他后果?
最后,当想到这种事情时,通常值得找出你正在使用的编译器实际生成的代码。大多数编译器都提供这样的机制。对于GCC,请了解“-S”选项,它将直接产生汇编代码而不是生成目标文件。

0

在汇编语言中,运算符<=和<是一条单独的指令,因此在性能上不应有差别。 需要注意的是,在某些处理器上,测试0可能比测试其他常量更快一些,因此可以合理地使循环向后运行:

int i = 10;
while (i != 0) 
{
    printf("%d\n", i);
    i--;
}

请注意,这样微小的优化通常只能为您带来非常少的性能提升,最好利用时间使用高效算法。

0
// Case I

int i = 0; 
while (i < 10) {
    printf("%d\n", i);
    i++;
    printf("%d\n", i);
    i++;
}

// Case II

int i = 0;
while (i < 10) {
    printf("%d\n", i);
    i++;
}

情况一的代码需要更多的空间,但速度快;情况二的代码需要较少的空间,但相对情况一的代码而言速度较慢。 因为在编程中,空间复杂度和时间复杂度总是成比例的。这意味着你必须在空间和时间之间做出权衡。
因此,你可以优化时间复杂度或空间复杂度,但不能同时兼顾两者。

而你的两个代码是相同的。


0

取决于体系结构和编译器。在大多数结构中,有一个单一指令用于 <= 或相反的操作,可以进行否定,因此如果将其转换为循环,则比较很可能只有一个指令。(在 x86 或 x86_64 上,它是一个指令)

编译器可能会将循环展开成十次 i++ 的序列,当涉及到常量表达式时,它甚至会优化掉 ++ 并只留下常量。

而且 Ira 是正确的,如果涉及到 printf,那么比较就会消失,而执行时间可能是数百万个时钟周期。


0
处理器会检查每次迭代中的(i < 10)和(i == 10)吗?还是只检查(i < 10),如果为真,就继续执行?
不是这两者,它很可能会检查(i < 11)。<= 10只是为了让您的代码更有意义,因为11是一个magic number,实际上表示(10+1)。

0
我正在用C语言编写一个循环,想知道如何进行一些优化。
如果你打开优化编译选项,最大的优化将来自于展开循环。
使用-O2对代码进行分析可能会比较困难,因为对于简单的函数,编译器会展开循环,你将无法测试实际比较中的差异。在测试使用常量的测试用例时,应该注意这些常量在编译器优化后可能使代码变得微不足道。

0

反汇编。根据处理器、优化和一些其他因素,这个简单的示例代码实际上会展开或执行与您真正问题无关的操作。使用gcc -O1编译,您提供的两个示例循环都会生成相同的汇编代码(对于arm而言)。

C代码中的小于符号通常会转换为分支(如果大于等于循环的远端)。如果您的处理器没有大于等于,则可能有一个大于分支和一个等于分支,即两个指令。

通常情况下,将会有一个寄存器保存变量i,然后有一条指令来增加i的值。然后会有一条指令来将i与10进行比较,判断是否相等、大于等于或小于一般都可以在单个指令中完成,因此您通常不应该看到差异。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接