【问题标题】:Iterating over all unsigned integers in a for loop在 for 循环中迭代所有无符号整数
【发布时间】:2017-03-18 21:31:22
【问题描述】:

假设我想遍历for 循环中的所有整数。为了便于讨论,假设我为每个整数调用了一些未知函数f(unsigned x)

for (unsigned i = 0; i < UINT_MAX; i++) {
     f(i);
}

当然,上面的方法无法遍历所有个整数,因为它错过了一个:UINT_MAX。将条件更改为i &lt;= UINT_MAX 只会导致无限循环,因为这是重言式。

您可以使用 do-while 循环来完成此操作,但您会失去 for 语法的所有细节。

我可以把我的蛋糕(for 循环)也吃掉吗(遍历所有整数)?

【问题讨论】:

  • 如何在do-while 循环中完成?
  • 没有“简单”的解决方案。使用“do-while”循环或等效循环。
  • @yano 喜欢unsigned i=0; do { f(i); } while (++i != 0)
  • 啊,,,,,好!
  • ... 是的,除了你失去了for 循环的很多好处 - 一个大的是将i 的范围限定为循环的主体,而更小的循环是更紧凑的初始化、终止检查、增量语法。

标签: c loops for-loop syntax


【解决方案1】:

您必须在循环体的末尾执行测试,就像 do-while:

for (unsigned int i = 0; /* nothing */; i++) {
    ...
    if (i == UINT_MAX) {
        break;
    }
}

要使标准 for 循环测试位置中的测试工作,您需要以一种可以区分 UINT_MAX+2 状态的方式跟踪当前迭代:每次进入循环体时一个,一个用于有一次你没有。单个 unsigned int 无法处理,因此您至少需要一个辅助变量或更大的循环计数器。

【讨论】:

  • 是的,在某些方面比 do-while 更混乱,但至少将 i 的范围限定为循环体。
  • 与 Barmar 之前的解决方案相比,this ends up 剥离了一次迭代,然后以2^32-1 的行程计数进行循环,使用 32 位计数器。我需要检查如果身体的环更大会发生什么,因为那样剥离会相对更糟。
  • @BeeOnRope:do-while 做什么?
  • 它最终使用类似于 Barmar 解决方案的组件。 See here - 使用 64 位计数器来解决“一个太多”问题。在那里,我通过四次调用f(i) 扩展了循环中的“工作”,这说明了为什么循环剥离解决方案不是很好:它确实扩展了代码大小。我不知道gcc 将停止像这样剥离第一次迭代的限制是多少。我还没有检查其他编译器!
  • FWIW,我想不出比 64 位计数器更好的汇编级解决方案。虽然它不适用于 64 位计数器,但实际上迭代需要花费不合理的年数 :)
【解决方案2】:

你可以用一个 do-while 循环来做,但是你失去了所有的细节 for 语法。

通过使用匿名块范围,do-while 循环仍然可行:

{
    unsigned i = 0;
    do { f(i); } while (++i != 0);
}

虽然这种结构可能不是最惯用的,但它显然是清晰的汇编代码的候选者。例如,gcc -O 将其编译为:

.L2:
        mov     edi, ebx   ; ebx starts with zero
        call    f
        add     rbx, 1
        cmp     rbx, rbp   ; rbp is set with 4294967296
        jne     .L2

【讨论】:

  • 没错,代价是多行几行和另一层缩进。
  • 是的,关于它编译良好的程序集,与其他似乎使用 64 位寄存器进行计数的最佳编译解决方案一致。尽管与clang 相比,gcc 总体而言似乎特别差。最简单的循环就是what clang does,实际上它至少可以减少一个循环。
  • 我已经接受了这个答案,因为我认为这是实现这一目标的最清晰的方法之一(来自 user2357112 的解决方案也很清楚)并且仍然有效地将 i 范围限定为循环体(不幸的是仅用于范围界定的额外块的成本)。决胜局是,今天的编译器似乎比for + explicit-break-at-end 方法更好,并将其编译为合理的代码。
  • gcc输出的优化程度较低,因为你实际上不需要将rbp设置为4294967296,可以直接使用增量后的零标志godbolt.org/g/BFDHSd
【解决方案3】:

您可以使用另一个变量来检测您何时循环。

for (unsigned int i = 0, repeat = 0; !(i == 0 && repeat); i++, repeat = 1) {
    ...
}

【讨论】:

  • 是的。我可以。我 似乎 非常糟糕 - 每次迭代都会执行一个附加变量和检查。对我来说有点令人惊讶的是,gcc 将其全部切入并编译了pretty much optimally。整个repeat 业务消失,循环仅使用 64 位计数器并与2^32 进行显式比较。
  • 哇,我也很惊讶。我实际上并不为这段代码感到自豪,它的作用并不明显。我更喜欢@user2357112 的解决方案。
  • 在 C-land 中它看起来更好,但到目前为止,该程序集似乎稍微偏向您的程序(至少在 gcc 上。请参阅我在 user23 上的 cmets... 回答。
  • 通常我会说不用担心生成的代码。但是,如果您要循环多次,那么每一点都很重要。每次迭代一微秒就会使总运行时间增加一个多小时。
【解决方案4】:

通过单个测试有效实现迭代的经典方法是 do / while 循环:

unsigned i = 0;
do { f(i); } while (i++ != UINT_MAX);

如果你坚持使用for 循环:

for (unsigned i = 0;; i++) {
    f(i);
    if (i == UINT_MAX)
        break;
}

这是另一个带有 2 个变量的变体,其中所有逻辑都在 for 表达式中:

for (unsigned int i = 0, not_done = 1; not_done; not_done = (i++ - UINT_MAX)) {
    f(i);
}

由于额外的变量,它可能会产生较慢的代码,但正如 BeeOnRope 评论的那样,clangicc 将其编译为 very efficient code

【讨论】:

【解决方案5】:

一个简单的解决方案是,

unsigned i;
for (i=0; i<UINT_MAX; i++) {
  f(i);
}
f(i);  // i will be UINT_MAX at this time.

【讨论】:

  • 仍然没有作用于内部的i 变量。恕我直言,do while 解决方案在这种情况下仍然是最好的
  • 另外,当f(i) 是一个简单的函数调用时,这看起来没问题,但在一般情况下,它可能是需要复制的多行代码,与 DRY 发生冲突并使其变得更糟比不重复循环体的解决方案。
【解决方案6】:

使用更大的整数类型:

#include <limits.h>
#include <stdio.h>

int main() {
    for (unsigned long i = 0; i <= UINT_MAX; i++) {
        f(i);
    }
}

此版本使用 stdint 以获得更高的一致性

#include <stdio.h>
#include <stdint.h>

int main() {
    for (uint_fast64_t i = 0; i <= UINT32_MAX; ++i) {
        f(i);
    }
}

【讨论】:

  • 这并不总是实用的,因为较大的整数类型可能会更慢并且占用更多内存。此外,您的循环仍然没有转到UINT_MAX。应该是i &lt; UINT_MAX + 1UL
  • 问题中提出的主要问题是保持for 循环的便利性,而不是效率或性能。不过,我已经修复了比较。
  • 您的“固定”版本不起作用,因为比较仍以unsigned long 类型完成,因此始终为真
  • unsigned long 可能不是更大的类型:例如,它与 Windows 64 位上的 unsigned int 大小相同。您将在这样的架构上进行无限循环。
  • FWIW,我很关心便利性和性能。任何时候你循环 40 亿次,性能都可能有点重要:)
猜你喜欢
  • 2011-07-24
  • 2012-02-21
  • 1970-01-01
  • 1970-01-01
  • 2023-03-15
  • 1970-01-01
  • 1970-01-01
  • 2016-10-10
相关资源
最近更新 更多