在'for'循环中递增1时格式化的技术原因?

har*_*ers 53 c c# c++ java for-loop

在整个网络上,代码示例都有如下所示的for循环:

for(int i = 0; i < 5; i++)
Run Code Online (Sandbox Code Playgroud)

我使用以下格式:

for(int i = 0; i != 5; ++i)
Run Code Online (Sandbox Code Playgroud)

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

Luc*_*cas 86

每个人都喜欢他们的微观优化,但就我所见,这并没有什么不同.对于英特尔处理器,我使用g ++编译了两个版本,没有任何花哨的优化,结果是

for(int i = 0; i < 5; i++)
Run Code Online (Sandbox Code Playgroud)
    movl $0, -12(%ebp)
    jmp L2
L3:
    leal    -12(%ebp), %eax
    incl    (%eax)
L2:
    cmpl    $4, -12(%ebp)
    jle L3
Run Code Online (Sandbox Code Playgroud)
for(int i = 0; i != 5; ++i)
Run Code Online (Sandbox Code Playgroud)
    movl    $0, -12(%ebp)
    jmp L7
L8:
    leal    -12(%ebp), %eax
    incl    (%eax)
L7:
    cmpl    $5, -12(%ebp)
    jne L8
Run Code Online (Sandbox Code Playgroud)

我认为jle并且jne应该转换为大多数架构上同样快速的指令.所以对于性能,你不应该区分这两者.总的来说,我同意第一个更安全,我也认为更常见.


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

第5.1.2.3节程序执行

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

但是有理由认为现代编译器会生成同样有效的代码,我认为在非常罕见的情况下,循环测试计数表达式将成为for循环的瓶颈.

至于味道,我写

for(int i = 0; i < 5; ++i)
Run Code Online (Sandbox Code Playgroud)

  • 这一点的重要部分是x86使用条件代码寄存器,因此它为任何比较计算相等和符号标志,因此所有积分/等式的条件跳转花费的时间完全相同. (6认同)

Yur*_*ich 69

如果由于某种原因i在循环中跳到50,你的版本将永远循环.这i < 5是一个健全检查.

  • 如果我在循环中被修改,事情已经够糟糕了,我可能会有一个无限循环来帮助我追踪正在发生的事情.说真的,如果有什么东西搞乱我,循环中应该有一个断言来找出答案.我更喜欢语言的另一个原因是让我尽可能地做一个foreach而不是for. (18认同)
  • 设置我打破循环?啊.如果_break_无法做到这一点,重构! (12认同)
  • 这就是我的观点,如果它发现需要在子句开头的某些业务逻辑中退出,那该怎么办呢?打破; 不会执行剩余的逻辑. (4认同)
  • ``=`上的```版本可以更容易编写循环不变量来证明正确性. (2认同)

Tom*_*icz 41

表格

for (int i = 0; i < 5; i++)
Run Code Online (Sandbox Code Playgroud)

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

在循环中修改i或使用不同于1的增量时,它也会更安全一些.但这是一个小问题.最好仔细设计你的循环并添加一些断言以尽早捕捉破坏的假设.

  • 形式`for(int i = 0; i!= 5; ++ i)`对于相当多的C++人来说更为惯用.实际上,我不知道你怎么能说任何一种C风格的语言都不是惯用的,它们都是所有这些语言中母亲的膝盖. (13认同)
  • +2用于讨论真实点(可读性,而不是性能)和单词"惯用语".最后这个词让我简单地表达了我的想法 - 两个惯用的`for`语句是`for(int i = 0; i <MAX; i ++)`和`for(type*p = first; p!= NULL ; p = p-> next)`.我认为应该尽可能地选择这两个. (3认同)
  • 我们不是在谈论C代码,这个问题非常明确地包括C++和C#以及隐式Java和Javascript.整数不是迭代器,但使用它们可以导致!=比<更本能.我仍然不能考虑使用这些语言中的任何一种惯用语,但它们都是你应该在所有这些语言中自然地理解所有语言的形式. (2认同)

Chr*_*nce 18

如果增量规则略有变化,则会立即产生无限循环.我更喜欢第一个结束条件.

  • 真正.在示例中转到i + = 2会导致痛苦. (7认同)

use*_*019 13

这取决于语言.

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

对于整数,要么形成作品.C的常见习语是第一个,而对于C++,它是第二个.

对于C#和Java的使用,我会循环遍历所有事情.

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


Cal*_*lum 12

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


Jon*_*nna 10

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

for(int i = 0; i < 5; i++)
for(int i = 0; i != 5; ++i)
Run Code Online (Sandbox Code Playgroud)

我们可以添加:

for(int i = 0; i < 5; ++i)
for(int i = 0; i != 5; i++)
Run Code Online (Sandbox Code Playgroud)

在大多数具有现代编译器的现代机器上,这些效率完全相同并不奇怪.可能有一天你可能会发现自己为一些小型处理器编程,而平等比较和低于比较之间存在差异.

在某些情况下,根据我们选择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++)
Run Code Online (Sandbox Code Playgroud)

这里的一个明显区别是,在两种情况下someType必须具有意义<,对于其余情况,它必须具有意义!=.!=定义的类型<并不常见,包括C++中的很多迭代器对象(可能在C#中,与STL迭代器相同的方法是可行的,有时也很有用,但既不是惯用的,也不是公共库直接支持的,也不是因为有竞争对手的习语有更直接的支持,所以常常很有用.值得注意的是,STL方法是专门设计的,以便在有效迭代器类型集中包含指针.如果您习惯使用STL,!=即使应用于整数,您也会考虑使用更加惯用的表单.就个人而言,接触它的量非常小,足以让我成为我的直觉.

另一方面,虽然定义<而不是!=更少,但它适用于我们用增量i值的不同增量替换增量或者在i循环内可能改变的情况.

因此,双方都有明确的案例,其中一种或另一种是唯一的方法.

现在对于++iVS i++.再次使用整数并且直接调用而不是通过返回结果的函数(并且偶然的机会),实际结果将完全相同.

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

C++和C#允许我们覆盖它们.通常,前缀的++作用类似于以下功能:

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

postfix ++的功能就像一个函数:

SomeType copy = Clone(val);
val = OneMoreThan(val);
return copy;
Run Code Online (Sandbox Code Playgroud)

C++和C#都不能完美地匹配上述内容(我故意不使我的伪代码匹配),但在任何一种情况下都可能有副本或者两个副本.这可能贵也可能不贵.它可能是可避免的,也可能是不可避免的(在C++中我们通常可以this通过返回void 返回和后缀中的前缀形式完全避免它).它可能会也可能不会被优化为任何东西,但它仍然++ii++在某些情况下更有效.

更具体地说,稍微提高一点性能的可能性很大++i,而且一个大类甚至可能是相当大的,但是除非有人压倒C++以使两者具有完全不同的含义(一个非常糟糕的主意)它通常不可能这是另一种方式.因此,养成在postfix上使用前缀的习惯意味着你可能会在一千次中获得一次改进,但不会失败,所以这是C++编码人员经常遇到的习惯.

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


das*_*ght 9

!=在阅读了Dijkstra的一本名为"编程学科"的书后,我转而使用了大约20多年.在他的书中,Dijkstra观察到较弱的延续条件导致环结构中更强的后置条件.

例如,如果我们修改你的构造以i在循环之后公开,那么第一个循环的后置条件将是i >= 5,而第二个循环的后置条件要强得多i == 5.这对于循环不变量,后置条件和最弱前提条件的形式化程序的推理更好.


Den*_*er8 6

我同意关于可读性的说法 - 重要的是让代码对于维护者来说很容易阅读,尽管你希望那些人能够理解增量前后的内容.

也就是说,我认为我会运行一个简单的测试,并获得一些关于四个循环中哪个循环运行最快的可靠数据.我是普通的规格电脑,用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

结果让我觉得 - 测试 i < maxValue每个循环大约需要1.39ms,无论是使用前增量还是后增量,但i != maxValue需要1.05ms.这是一个节省24.5%或32.5%的时间,取决于你如何看待它.

当然,运行for循环所需的时间可能不是你的瓶颈,但这是一种有用的优化,对于你需要它的罕见情况.

不过我认为我仍然坚持不到测试!

编辑

我测试过递减我为好,并发现这并没有真正有需要个时间的影响- for (int i = 2000000; i != 0; i--)以及for (int i = 0; i != 2000000; i++)两者采取相同的时间长度,因为这样做for (int i = 2000000; i > 0; i--)for (int i = 0; i < 2000000; i++).


har*_*ers 4

我决定列出最有信息性的答案,因为这个问题变得有点拥挤。

DenverCoder8的基准测试以及Lucas的循环编译版本显然值得一些认可。Tim Gee展示了前后增量之间的差异,而User377178则强调了 < 和 != 的一些优缺点。Tenacious Techhunter撰写了有关循环优化的一般性文章,值得一提。

这就是我的 5 个最佳答案。

  1. 丹佛编码器8
  2. 卢卡斯
  3. 蒂姆·吉
  4. 用户377178
  5. 顽强的技术猎手