在“for”循环中递增 1 时格式化背后的技术原因?

Posted

技术标签:

【中文标题】在“for”循环中递增 1 时格式化背后的技术原因?【英文标题】:Technical reasons behind formatting when incrementing by 1 in a 'for' loop? 【发布时间】:2010-12-19 12:31:40 【问题描述】:

在整个网络上,代码示例都有for 循环,看起来像这样:

for(int i = 0; i < 5; i++)

虽然我使用了以下格式:

for(int i = 0; i != 5; ++i)

我这样做是因为我认为它更有效,但在大多数情况下这真的很重要吗?

【问题讨论】:

我想说,对于大多数编译器和处理器来说,你的循环一点也不快。使用第一个版本,以便其他程序员更容易理解循环。现在,倒计时到零帮助您处理某些处理器,但您必须问问自己是否值得。 您认为它更高效的原因是什么?你说的是运行时间,对吗? 在大多数语言中,第一种形式会更常见(并且在效率方面没有区别)。在 C++ 中,第二种形式更为常见,因为它可以与前向/双向迭代器以及随机访问迭代器一起使用,其中第二种形式仅适用于随机访问迭代器。 @Earlz 他并没有编写 Z80 汇编代码。如果一个比较比另一个快,编译器可能会使用更快的比较,而不管他的代码是什么。 @harryovers 您在当前答案中遗漏了哪些细节?卢卡斯的回答似乎很详细。 【参考方案1】:

如果由于某种原因i 在循环中跳转到 50,您的版本将永远循环。 i &lt; 5 是一个健全性检查。

【讨论】:

如果我在循环中被修改,事情已经够糟糕了,我可能宁可有一个无限循环来帮助我追踪正在发生的事情。说真的,如果 i 有什么问题,应该在循环中有一个断言来找出答案。我更喜欢让我尽可能使用 foreach 而不是 for 的语言的另一个原因。 设置 i 跳出循环?啊。如果 break 不起作用,请重构! 这就是我的意思,如果它发现它需要在子句开头的某些业务逻辑中退出怎么办。休息;不会执行剩余的逻辑。 @Bill 你必须使用 goto 语句(说“使用 goto 是正确的方法”真是太好了,我很少有这种能力)。 &lt; 版本比 != 更容易编写循环不变量来证明正确性。【参考方案2】:

嗯...只要您不在 for 循环中修改 i 就可以了。真正的“最佳”语法完全取决于您想要的结果。

【讨论】:

【参考方案3】:

如果增量规则略有变化,您将立即进入无限循环。我更喜欢第一个结束条件。

【讨论】:

是的。在示例中转到 i+=2 会导致痛苦。【参考方案4】:

表格

for (int i = 0; i < 5; i++)

惯用的,因此对于有经验的 C 程序员来说更容易阅读。 特别是当用于迭代数组时。 您应该尽可能编写惯用代码,因为它读起来更快。

当您在循环内修改 i 或使用不同于 1 的增量时,它也会更安全一些。 但这是一件小事。 最好仔细设计您的循环并添加一些断言以尽早发现错误的假设。

【讨论】:

for (int i = 0; i != 5; ++i) 的形式对于很多 C++ 人来说会更惯用。不过,真的,我不能说这两种语言在任何 C 风格语言中都不是惯用的,在所有这些语言中,它们都是母亲的膝盖。 @JonHanna 至于你评论的第二部分:我从 K&R 学习了 C,这是我见过的最常用的版本。我发现它一目了然。至于第 1 部分:您是说带有 C++ 迭代器的 for 循环看起来像这样:for (some::long_iter_type it = begin; it != end; ++it) ...?如果是这样,那么:a)我们主要是在谈论 C 代码。 b) 即使在 C++ 中,int i 也不是迭代器。 我们不是在谈论 C 代码,这个问题非常明确地包括 C++ 和 C#,隐含地包括 Java 和 javascript。整数不是迭代器,但使用它们会导致 != 比 @JonHanna 对。你让我了解语言部分。但迭代整数范围的基本习惯用法仍然来自 C。在这里我看不出!= 会被认为是直观的。也许我在生活中做了太多的数学运算,但对我来说,要么是&lt; 要么是&lt;=---如0 &lt;= i &lt; n0 &lt;= i &lt;= n-1---我更喜欢前者。 +2 用于谈论真正的观点(可读性,而不是性能)和“惯用”一词。这个词终于让我简单地表达了我的这个想法——两个惯用的for语句是for (int i=0; i&lt;MAX; i++)for (type *p=first; p!=NULL; p=p-&gt;next)。我认为一个人应该尽可能喜欢这两个。【参考方案5】:

我认为第二个可读性较差(如果只是因为“标准”做法似乎是前者)。

【讨论】:

【参考方案6】:

这取决于语言。

C++ 文本通常建议使用第二种格式,因为它适用于可以直接比较 (!=) 但不能使用大于或小于条件的迭代器。此外,前增量可以比后增量更快,因为不需要变量的副本进行比较 - 但是优化器可以处理这个问题。

对于整数,任何一种形式都可以。 C 的常用习语是第一个,而 C++ 是第二个。

对于 C# 和 Java 使用,我会 foreach 循环所有东西。

在 C++ 中,还有 std::for_each 函数需要使用函子,对于简单的情况,这可能比这里的任何一个示例都更复杂,而 Boost FOR_EACH 可能看起来像 C# foreach 但内部很复杂。

【讨论】:

虽然我同意,但 OP 正试图通过区分 !=&lt; 调用来进行微优化,所以我怀疑他们是否想为 foreach 调用引入迭代器的开销!【参考方案7】:

关于使用 ++i 代替 i++,它与大多数编译器没有区别,但是当用作迭代器时,++i 可能比 i++ 更有效。

【讨论】:

同意。当您在迭代器上使用后增量而不是前增量时,诸如“cppcheck”之类的软件实际上会警告您。【参考方案8】:

我永远不会这样做:

for(int i = 0; i != 5; ++i)

i != 5 为 i 永远不会是 5 的可能性打开它。跳过它并遇到无限循环或数组访问器错误太容易了。

++i

虽然很多人都知道可以把 ++ 放在前面,但也有很多人不知道。代码需要对人们可读,虽然它可以是使代码运行得更快的微优化,但当有人必须修改代码并弄清楚为什么要这样做时,这真的不值得额外的头痛。

我认为 Douglas Crockford 有最好的建议,那就是根本不要使用 ++ 或 --。有时它会变得太混乱(可能不在循环中,但肯定在其他地方),写 i = i + 1 也很容易。他认为摆脱它只是一个坏习惯,而我有点在看到一些残暴的“优化”代码后同意。

我认为 crockford 的目的是使用这些运算符,您可以让人们编写如下内容:

var x = 0;
var y = x++;

y = ++x * (Math.pow(++y, 2) * 3) * ++x;

alert(x * y);

//答案是 54 顺便说一句。

【讨论】:

我不能再反对你了。我希望知道前缀与后缀增量是我接受维护程序员的绝对最低限度的知识之一。我认为应该尽可能使用它,因为很容易以编译器无法捕获的方式错误输入 i = i + 1。 如果人们不懂++i,他们就没有业务编写代码。 您的建议不适用于 C++,因为在 C++ 中存在您非常肯定想要 for (i = ...; i != ...; ++i) 的情况,如果 i 不是随机访问迭代器和/或 i相当复杂。养成良好的习惯而不是错过某些东西和编码不当是有用的。 我原以为++i 会比i++ 更常见,如果只是因为在那些会产生影响的情况下-您将值用作更广泛表达式的一部分-稍微有点比旧的更关心新的价值。 y = ++x * (Math.pow(++y, 2) * 3) * ++x; 的行为未定义;它修改x 两次,没有中间的序列点。而如果我在C代码中看到i = i + 1;,我的第一反应就是作者没有学过这门语言。【参考方案9】:

每个人都喜欢他们的微优化,但据我所知,这并没有什么不同。我为英特尔处理器使用 g++ 编译了两个变体,没有任何花哨的优化,结果适用于

for(int i = 0; i < 5; i++)
    movl $0, -12(%ebp)
    jmp L2
L3:
    leal    -12(%ebp), %eax
    incl    (%eax)
L2:
    cmpl    $4, -12(%ebp)
    jle L3

for(int i = 0; i != 5; ++i)
    movl    $0, -12(%ebp)
    jmp L7
L8:
    leal    -12(%ebp), %eax
    incl    (%eax)
L7:
    cmpl    $5, -12(%ebp)
    jne L8

我认为jlejne 应该在大多数架构上转换为同样快速的指令。 所以为了性能,你不应该区分两者。总的来说,我同意第一个更安全一些,我也认为更常见。


编辑(2 年后):由于这个帖子最近再次引起了很多关注,我想补充一点,一般很难回答这个问题。 C-Standard [PDF] 没有明确定义哪些代码版本更有效(这同样适用于 C++,可能也适用于 C#)。

第 5.1.2.3 节程序执行

§1本国际标准中的语义描述描述了抽象机器的行为,其中优化问题无关紧要。

但可以合理地假设现代编译器将产生同样高效的代码,我认为只有在极少数情况下,循环测试计数表达式成为for循环的瓶颈。

至于味道,我写

for(int i = 0; i < 5; ++i)

【讨论】:

@harryovers:我不得不说,我不会被你的方式弄糊涂的。我认为发生了什么很明显,但如果你没有性能问题,你不应该为小的优化而努力。 我并不担心我只是想知道人们的想法 其中重要的部分是 x86 使用条件代码寄存器,因此它会为任何比较计算相等和符号标志,因此所有积分 in/等式的条件跳转所花费的时间完全相同.【参考方案10】:

如果您的索引不是int,而是(例如)C++ 类,那么第二个示例可能会更高效。

但是,正如所写,您认为第二种形式更有效的看法是完全错误的。任何体面的编译器都会为简单的 for 循环提供出色的代码生成习惯,并为任一示例生成高质量的代码。更重要的是:

在执行繁重的性能关键型计算的 for 循环中,索引算法在总负载中几乎可以忽略不计。

如果您的 for 循环对性能至关重要,并且不进行繁重的计算以致索引算术实际上很重要,那么您几乎可以肯定地重组您的代码,以便在循环的每一轮中做更多的工作。

【讨论】:

如果索引不是随机访问迭代器(在 C++ 中),第一个版本甚至都不正确。【参考方案11】:

在您的代码中散布数字文字?惭愧……

重回正轨,Donald Knuth 曾经说过

我们应该忘记小 效率,说大约 97% 时间:过早优化是 万恶之源。

所以,归根结底是哪个更容易解析

那么...综合以上两点,以下哪一项对程序员来说更容易解析?

for (int i = 0; i < myArray.Length; ++i)

for (int i = 0; i != myArray.Length; ++i)

编辑:我知道 C# 中的数组实现了 System.Collections.IList 接口,但在其他语言中不一定如此。

【讨论】:

在第二个例子中为什么要少迭代一次? 我不会做“i != myArray.Length - 1”。我更喜欢 "i = 0, n = myArray.Length; i != n" 好答案。当然,我们不知道他正在遍历整个数组。事实上,有了那个数字文字,我们不知道他在做什么。 这种情况下倒计时可能会更快,具体取决于获取数组长度的速度。 @UncleBens:你不会的。 Braino 是我的一部分。【参考方案12】:

关于可读性。作为一个喜欢 Ruby 的 C# 程序员,我最近为 int 编写了一个扩展方法,它允许以下语法(如在 Ruby 中):

4.Times(x => MyAction(x));

【讨论】:

【参考方案13】:

在通用代码中,您应该更喜欢带有!= 运算符的版本,因为它只要求您的i同等可比,而&lt; 版本要求它是关系可比。后者是比前者更强的要求。当一个较弱的要求完全足够时,您通常应该更愿意避免较强的要求。

话虽如此,在您的具体情况下,如果int i 两者都可以同样良好地工作并且性能不会有任何差异。

【讨论】:

【参考方案14】:

当我第一次开始使用 C 编程时,我在 for 循环中使用了 ++i 形式,只是因为我当时使用的 C 编译器没有进行太多优化,并且在这种情况下会生成稍微更高效的代码。

现在我使用++i 形式,因为它读作“递增 i”,而 i++ 读作“递增 i”,任何英语老师都会告诉你避免使用被动语态。

底线是做任何对你来说更易读的事情。

【讨论】:

【参考方案15】:

在这些情况下关心效率并不是一个好主意,因为您的编译器通常足够聪明,可以在可能的时候优化您的代码。

我曾在一家为安全关键系统生产软件的公司工作,其中一条规则是循环应该以“

    您的控制变量可能会因某些硬件问题或某些内存入侵而跳到更高的值;

    在维护过程中,可以增加循环内的迭代器值,或者执行“i += 2”之类的操作,这会使循环永远滚动;

    如果由于某种原因您的迭代器类型从“int”更改为“float”(我不知道为什么有人会这样做,但无论如何...)对浮点数进行精确比较是一种不好的做法.

(MISRA C++ 编码标准(针对安全关键系统)还告诉您在规则 6-5-2 中更喜欢“

关于 ++i 或 i++,我更喜欢使用 ++i。当您使用基本类型时,这没有区别,但是当您使用 STL 迭代器时,预增量更有效。所以我总是用 preincrement 来适应它。

【讨论】:

【参考方案16】:

使用 as != 5 不是很好的方法。但是

for (int i =0; i<index; ++i)

更有效率
for(int i=0; i<index; i++)

因为i++首先执行复制操作。有关详细信息,您可以查看 C++ 中的运算符重载。

【讨论】:

第三个表达式的结果被丢弃,所以性能上应该不会有任何差异;编译器应该认识到它只需要副作用。我希望两者都生成相同的代码,至少在启用优化的情况下。 (如果您正在调用重载的“++”运算符,则不适用。)【参考方案17】:

实际上你给的东西有四种排列。给你们两个:

for(int i = 0; i < 5; i++)
for(int i = 0; i != 5; ++i)

我们可以添加:

for(int i = 0; i < 5; ++i)
for(int i = 0; i != 5; i++)

在大多数带有现代编译器的现代机器上,它们的效率完全相同也就不足为奇了。有朝一日,您可能会发现自己正在为一些小型处理器编程,其中相等比较和小于比较之间存在差异。

在某些情况下,根据我们选择 0 和 5 的原因,在特定情况下考虑“小于”或“不等于”可能更有意义,但即便如此,是什么让一个看起来对一个编码员来说是显而易见的,而对另一个编码员来说可能不是。

更抽象地说,这些是以下形式:

for(someType i = start; i < end; i++)
for(someType i = start; i != end; ++i)
for(someType i = start; i < end; ++i)
for(someType i = start; i != end; i++)

这里的一个明显区别是,在两种情况下,someType 必须对 &lt; 有意义,而在其他情况下,它必须对 != 有意义。定义了!=&lt; 不是很常见的类型,包括C++ 中的相当多的迭代器对象(并且可能在C# 中,与STL 迭代器相同的方法是可能的并且有时有用,但都不是惯用的,由公共库直接支持,也不是经常有用,因为有更直接支持的竞争对手习语)。值得注意的是,STL 方法经过专门设计,以便在有效迭代器类型集中包含指针。如果您有使用 STL 的习惯,您会考虑使用 != 的形式,即使应用于整数时也更加惯用。就个人而言,接触它的次数非常少就足以让它成为我的本能。

另一方面,虽然定义 &lt; 而不是 != 会更罕见,但它适用于我们用 i 的值的不同增加来替换增量的情况,或者 i 可能在循环内改变。

因此,双方都有明确的情况,其中一种或另一种是唯一的方法。

现在是 ++ii++。再次使用整数,当直接调用而不是通过返回结果的函数时(甚至有可能),实际结果将完全相同。

在一些 C 风格的语言(那些没有运算符重载的语言)中,整数和指针是唯一存在的情况。我们几乎可以人为地发明一种情况,通过一个函数调用增量只是为了改变它的运行方式,而且编译器可能仍然会将它们变成相同的东西。

C++ 和 C# 允许我们覆盖它们。通常前缀++ 像一个函数一样运行:

val = OneMoreThan(val);//whatever OneMoreThan means in the context.
//note that we assigned something back to val here.
return val;

而后缀++ 的作用类似于一个函数:

SomeType copy = Clone(val);
val = OneMoreThan(val);
return copy;

C++ 和 C# 都不能完美地匹配上面的内容(我故意让我的伪代码都不匹配),但在任何一种情况下,都可能有一个副本或两个副本。这可能会也可能不会很贵。它可能会也可能不会避免(在 C++ 中,我们通常可以通过返回 this 来完全避免前缀形式,并通过返回 void 在后缀中完全避免)。它可能会或可能不会被优化到零,但在某些情况下,使用++i 可能比i++ 更有效。

更具体地说,++i 有轻微的性能改进的可能性,并且对于一个大类甚至可能是相当大的,但除非有人在 C++ 中重写,这样两者的含义就完全不同(一个非常糟糕的主意) 通常不可能反过来。因此,养成偏好前缀而不是后缀的习惯意味着您可能会获得千分之一的改进,但不会失败,因此这是 C++ 编码人员经常养成的习惯。

总而言之,您的问题中给出的两种情况绝对没有区别,但可以有相同的变体。

【讨论】:

【参考方案18】:

我认为最终归结为个人喜好。 我喜欢这个想法

for(int i = 0; i < 5; i++)

结束

for(int i = 0; i != 5; ++i)

由于某种原因,i 的值有可能超过 5。我知道大多数时候这种情况发生的可能性很小,但我认为最终它是一种很好的做法。

【讨论】:

【参考方案19】:

总结两种方案的优缺点

!=的优点

当 int 被一些迭代器或通过模板参数传递的类型替换时,它更有可能工作,它会做预期的事情并且效率更高。 如果允许错误检测的 i 变量发生意外情况,将“永远循环”

的优点

正如其他人所说的那样,使用简单类型与其他人一样有效 如果在循环中增加 i 或将 5 替换为在循环运行时被修改的某个表达式,它将不会“永远”运行 适用于浮点类型 更具可读性 - 习惯问题

我的结论:

    也许应该在大多数情况下使用 != 版本,当 i 是离散的并且比较的另一方不打算在环形。

    虽然 的存在清楚地表明 i 是简单类型(或计算为简单类型)并且条件并不简单: i 或条件在循环和/或并行处理中被额外修改。

【讨论】:

不确定卡在无限循环中的错误检测属性。并不是说不管怎样都更好。所以也许在循环之后Assert(i==length-1); 如果这是一个问题。【参考方案20】:

我们可以再使用一个技巧。

for (i = 5; i > 0; i--)

我想大多数编译器都会像这样优化循环。 我不知道。请有人验证。

【讨论】:

你是说你认为这样更优化吗?并且编译器会采用 for (i = 0; i &lt; 5; i++) 并用它替换? 好吧,如果i 被用来索引一个数组(例如),那么你的循环将被反转。没有编译器会这样做。但是,如果 i 没有在 for 循环内(或在它之后)被引用,我想这将取决于编译器。然而,对于像这样的固定迭代循环,编译器通常可以选择展开它们。即在这种情况下,它将复制和粘贴内部代码 5 次,而不是循环。这是代码大小和速度之间的权衡,通常可以在编译器中自定义。【参考方案21】:

似乎没有人说明原因为什么从历史上看,对于小循环,前置增量运算符++i 比后缀i++ 更受欢迎。

考虑前缀(增量和获取)和后缀(获取和增量)的典型实现:

// prefix form: increment and fetch
UPInt& UPInt::operator++()

   *this += 1;      // increment
   return *this;    // fetch


// posfix form: fetch and increment
const UPInt UPInt::operator++(int)

   const UPInt oldValue = *this;
   ++(*this);
   return oldValue;
 

请注意,前缀操作可以就地完成,因为后缀需要另一个变量来跟踪旧值。如果您不确定为什么会这样,请考虑以下几点:

int a = 0;
int b = a++; // b = 0, the old value, a = 1 

在一个小循环中,后缀所需的这种额外分配理论上可能会使其变慢,因此老派的逻辑是前缀更有效。因此,很多 C/C++ 程序员都坚持使用前缀形式的习惯。

然而,在别处指出的事实是现代编译器很聪明。他们注意到在 for 循环中使用后缀形式时,不需要后缀的返回值。因此,没有必要跟踪旧值,并且可以对其进行优化 - 保留与使用前缀形式相同的机器代码。

【讨论】:

【参考方案22】:

实际上,在 Tim Gee 提供的 C++ 示例之前,还有一个更“历史”的原因:该机制起源于 C 机器上的具有前后递增寄存器指令的机器,而 C 语言最初旨在使“对机器编码”比使用汇编语言更容易维护。

虽然许多当前的编译器确实经常会忽略任何额外的副本,但最好还是根据您的特定环境和要求学习语言并选择操作,而不是盲目地跟随别人的例子。

对于它的价值,我会将循环索引定义为 unsigned 而不是 int 因为它更好地定义了简单循环计数器的有效域,并且会在以下情况下保护您i 的值会变成负数……

特别是在 C 的情况下,引用的基础语言,排序比较器是有意义的,因为它提供了安全性,并且因为机器针对寄存器(整数)值比较进行了优化,但它也付诸实践 主要是因为习惯使用从零开始的数组索引

使用 i != length 实际上是一个错误,因为您需要编写 i != (length - 1)。这当然会非常低效,因为您要么需要预先计算并将 (length - 1) 存储在临时变量中,从而“浪费”宝贵的寄存器资源,要么最终会重新计算它每次循环比较器——更糟糕的是faux pas

更重要的是,我正在回答这个问题,因为我认为对这个问题的“最佳”答案的“奖励”是毫无意义的。重要的是要避免试图根据论坛上经常被称为“火焰战争”或“宗教战争”的主题来定义“全球”解决方案。为什么?

如果您花一点时间研究 C++ 中的模板习语或 C# 和 Java 中的泛型,您将了解到排序和相等比较器并不总是基本的。根据排序运算符定义(不)相等运算符是很常见的,因为 它们 通常是使用 STL 或通用容器所必需的。在那些环境中,假设您可以定义“最有效”的源语法以用于从C基础派生的所有语言是非常危险的,您必须仔细决定当您利用预定义算法时使用哪些比较。

迭代器循环语法的“改变”是基于希望所有迭代器具有相同语法的愿望,排序比较器根本不适用于除了伪装成迭代器的数组指针之外的任何东西——这是唯一的情况容器的连续访问元素保证存储在具有单调递增地址的连续内存中。

如果效率真的对您很重要,如原始问题中所述,请根据数据衡量(配置文件)并优化您的使用情况。 “信念”的原始陈述是无关紧要的——衡量……从那时起,不要假设你在一个环境中的结论对所有其他环境都是合理的。

如果您追求源代码一致性,请查看来自不同组织的任意数量的编码建议,然后根据自己的喜好选择一个 - 只需准备好发现许多编码“标准”在很大程度上与其他标准正交......简而言之,这是一个愚蠢练习部的问题:-/

【讨论】:

那里有很多很好的信息,但实际上与问题i != length - 1 相关的部分实际上是错误的。 -1 是的,我同意这是错误的,因此具有误导性。我不能投票,但你应该编辑答案。【参考方案23】:

大约 20 多年前,在阅读了 Dijkstra 的名为 "A Discipline of Programming" 的书后,我转而使用 !=。在他的书中,Dijkstra 观察到 较弱 延续条件会导致循环构造中的更强 后置条件。

例如,如果我们修改您的构造以在循环后公开i,则第一个循环的后置条件将是i &gt;= 5,而第二个循环的后置条件要强得多i == 5 .这更适合以循环不变量、后置条件和最弱前置条件的形式对程序进行推理。

【讨论】:

【参考方案24】:

最终,更高效的决定因素既不是语言也不是编译器,而是底层硬件。如果您正在为 8051 等嵌入式微控制器编写代码,向上计数与向下计数,大于或小于与不等于,以及递增与递减,可以对性能产生影响,在循环的非常有限的时间范围内。

虽然足够的语言和编译器支持可以(并且经常这样做)减少以最佳但概念上等效的方式实现指定代码所需的指令的缺失,但硬件本身的编码保证性能,而不是仅仅希望在编译时存在足够的优化。

所有这一切意味着,您的问题没有一个通用的答案,因为市面上有很多不同的低端微控制器。

然而,比优化 for 循环的迭代、循环和中断方式更重要的是,修改它在每次迭代中的作用。如果导致 for 循环一个额外的指令在每次迭代保存两个或更多指令,执行它!您将获得一个或多个周期的净收益!对于真正优化的代码,您必须权衡完全优化 for 循环如何迭代每次迭代所发生的后果。

话虽如此,一个好的经验法则是,如果您发现记住特定目标硬件的所有汇编指令是一个挑战,那么“for”循环的所有变体的最佳汇编指令可能已经充分考虑。你可以随时检查你是否真的在乎。

【讨论】:

【参考方案25】:

我看到很多使用发布的特定代码和整数的答案。然而,这个问题是针对“for循环”的,而不是原始帖子中提到的具体问题。

我更喜欢使用前缀递增/递减运算符,因为它几乎可以保证与后缀运算符一样快,但在与非原始类型一起使用时可能会更快。对于像整数这样的类型,任何现代编译器都不重要,但如果您养成使用前缀运算符的习惯,那么在它提供速度提升的情况下,您会从中受益。

我最近在一个大型项目(可能大约 1-2 百万行代码)上运行了一个静态分析工具,它发现了大约 80 个使用后缀的情况,而前缀可以提供速度优势。在大多数情况下,好处很小,因为容器的大小或循环的数量通常很小,但在其他情况下,它可能会迭代超过 500 多个项目。

根据递增/递减对象的类型,当后缀出现时,也会出现副本。我很想知道有多少编译器会发现在未引用其值时使用后缀的情况,因此无法使用副本。在这种情况下它会为前缀生成代码吗?甚至静态分析工具也提到,它发现的 80 个案例中的一些可能无论如何都被优化了,但为什么要冒险让编译器决定呢?我发现前缀运算符在单独使用时一点也不令人困惑,它只会在开始使用时成为阅读负担,内联,作为逻辑语句的一部分:

int i = 5;
i = ++i * 3;

对于简单的逻辑,不必考虑运算符的优先级。

int i = 5;
i++;
i *= 3;

当然,上面的代码多出了一行,但读起来更清楚。但是对于一个 for 循环,被改变的变量是它自己的语句,所以你不必担心它是前缀还是后缀,就像在上面的代码块中一样,i++ 是单独的,所以几乎不需要考虑会发生什么,所以下面的代码块可能同样可读:

int i = 5;
++i;
i *= 3;

正如我所说,这并不重要,但是在我看来,在同一语句中没有使用变量时使用前缀只是一个好习惯,因为在某些时候你会在非原始类上使用它,您可能会为自己节省复制操作。

只要我的两分钱。

【讨论】:

在 C/C++ 中,“i = ++i * 3”实际上是未定义的 - 您在没有中间序列点的情况下修改了变量“i”两次。 有趣,猜猜那会加强这一点。我假设 ++ 将首先出现,因为它具有更高的优先级,然后是乘法。【参考方案26】:

在许多架构上,检查某个值是否为零要比检查它是否为其他任意整数要容易得多,因此,如果您真的想优化某些东西,请尽可能计数 down ,而不是(这是 ARM 芯片上的 example)。

总的来说,这实际上取决于您对数字和计数的看法。我在做大量的 DSP 和数学,所以从 0 数到 N-1 对我来说更自然,在这方面你可能会有所不同。

【讨论】:

【参考方案27】:

我同意关于可读性的说法 - 拥有易于维护人员阅读的代码很重要,尽管您希望无论是谁都能理解前增量和后增量。

也就是说,我认为我应该运行一个简单的测试,并获得一些关于四个循环中哪个循环运行速度最快的可靠数据。 我在一台普通规格的计算机上,使用 javac 1.7.0 进行编译。

我的程序创建了一个 for 循环,迭代了 2,000,000 次(以免将有趣的数据淹没在执行 for 循环中的任何操作所需的时间)。它使用上面提出的所有四种类型,并对结果进行时间计算,重复 1000 次以获得平均值。

实际代码是:

public class EfficiencyTest

public static int iterations = 1000;

public static long postIncLessThan() 
    long startTime = 0;
    long endTime = 0;
    startTime = System.nanoTime();
    for (int i=0; i < 2000000; i++) 
    endTime = System.nanoTime();
    return endTime - startTime;


public static long postIncNotEqual() 
    long startTime = 0;
    long endTime = 0;
    startTime = System.nanoTime();
    for (int i=0; i != 2000000; i++) 
    endTime = System.nanoTime();
    return endTime - startTime;


public static long preIncLessThan() 
    long startTime = 0;
    long endTime = 0;
    startTime = System.nanoTime();
    for (int i=0; i < 2000000; ++i) 
    endTime = System.nanoTime();
    return endTime - startTime;


public static long preIncNotEqual() 
    long startTime = 0;
    long endTime = 0;
    startTime = System.nanoTime();
    for (int i=0; i != 2000000; ++i) 
    endTime = System.nanoTime();
    return endTime - startTime;


public static void analyseResults(long[] data) 
    long max = 0;
    long min = Long.MAX_VALUE;
    long total = 0;
    for (int i=0; i<iterations; i++) 
        max = (max > data[i]) ? max : data[i];
        min = (data[i] > min) ? min : data[i];
        total += data[i];
    
    long average = total/iterations;

    System.out.print("max: " + (max) + "ns, min: " + (min) + "ns");
    System.out.println("\tAverage: " + (average) + "ns");


public static void main(String[] args) 
    long[] postIncLessThanResults = new long [iterations];
    long[] postIncNotEqualResults = new long [iterations];
    long[] preIncLessThanResults = new long [iterations];
    long[] preIncNotEqualResults = new long [iterations];

    for (int i=0; i<iterations; i++) 
        postIncLessThanResults[i] = postIncLessThan();
        postIncNotEqualResults[i] = postIncNotEqual();
        preIncLessThanResults[i] = preIncLessThan();
        preIncNotEqualResults[i] = preIncNotEqual();
    
    System.out.println("Post increment, less than test");
    analyseResults(postIncLessThanResults);

    System.out.println("Post increment, inequality test");
    analyseResults(postIncNotEqualResults);

    System.out.println("Pre increment, less than test");
    analyseResults(preIncLessThanResults);

    System.out.println("Pre increment, inequality test");
    analyseResults(preIncNotEqualResults);
    

对不起,如果我复制错了!

结果让我大吃一惊 - 测试 i &lt; maxValue 每个循环大约需要 1.39 毫秒,无论是使用前增量还是后增量,但 i != maxValue 需要 1.05 毫秒。那是节省 24.5% 或损失 32.5% 的时间,具体取决于您的看法。

当然,for 循环运行需要多长时间可能不是您的瓶颈,但这是一种很有用的优化,在您需要它的极少数情况下。

不过,我想我仍然会坚持测试不到的时间!

编辑

我也测试过递减 i ,发现这并没有真正影响它所花费的时间 - for (int i = 2000000; i != 0; i--)for (int i = 0; i != 2000000; i++) 都需要相同的时间长度,for (int i = 2000000; i &gt; 0; i--)for (int i = 0; i &lt; 2000000; i++).

【讨论】:

您是否暗示使用 javac 1.7.0 编译将优化循环中的前后增量,但不会优化结束条件 我还使用 C# 和 .NET4 中相同代码的略微修改版本进行了一些测试,结果表明无论使用什么格式,编译器都会优化循环,因为小于 0.05它们之间的毫秒数 @harryovers 是的,这正是我的结果所显示的。我运行了几次,每次运行都给出了相同的结果(实验错误)。 抱歉,这含糊不清 - 我同意您的第一条评论。你在 C# 和 .NET 中发现的正是我所期望的——编译器意识到它们都是一样的! 在运行基本相同的代码时,看看不同的语言和编译器如何比较肯定会很有趣【参考方案28】:

我记得有一个代码段,其中 i 由于某些错误而增加了 2 而不是 1,这导致它进入无限循环。所以最好有这个循环,如第一个选项所示。这也更具可读性。因为 i != 5 和 i

【讨论】:

【参考方案29】:

我决定列出最有用的答案,因为这个问题有点拥挤。

DenverCoder8 的基准测试以及Lucas 的循环编译版本显然值得一些认可。 Tim Gee 显示了前后增量之间的差异,而 User377178 强调了 Tenacious Techhunter 写过关于循环优化的文章,值得一提。

你有我的前 5 个答案。

    DenverCoder8 Lucas Tim Gee User377178 Tenacious Techhunter

【讨论】:

【参考方案30】:

FORTRAN 的 DO 循环和 BASIC 的 FOR 循环为正增量实现了 &lt;(实际上是 &lt;=)。不确定 COBOL 做了什么,但我怀疑它是相似的。因此,这种方法对于 C 等“新”语言的设计者和用户来说是“自然的”。

此外,&lt;!= 更有可能在错误情况下终止,并且对于整数和浮点值同样有效。

上面的第一点是样式开始的可能原因,第二点是它继续存在的主要原因。

【讨论】:

以上是关于在“for”循环中递增 1 时格式化背后的技术原因?的主要内容,如果未能解决你的问题,请参考以下文章

python中for循环怎么从1到4每行递增

什么会导致for循环在它应该递增时递减?

当push()在for循环中运行时浏览器崩溃的原因如下所示[复制]

javascript中的for(;;)后面的括号中的符号是啥意思?

Vue中v-for循环遍历静态图片时图片不能加载的原因和解决办法

在 for 循环中递增 JavaScript 对象中的值