遍历数组的简单任务.以下哪种解决方案最有效

The simple task of iterating through an array. Which of these solutions is the most efficient?

本文关键字:解决方案 有效 数组 简单任务 遍历      更新时间:2023-10-16

最近,我一直在思考可以遍历数组的所有方法,并想知道其中哪一种效率最高(和最低)。我写了一个假设的问题和五个可能的解决方案。

问题

给定一个int数组arr len元素数量,为每个元素分配任意数量的42的最有效方法是什么?

解决方案0:显而易见的

for (unsigned i = 0; i < len; ++i)
    arr[i] = 42;

解决方案1:显而易见的反向

for (unsigned i = len - 1; i >= 0; --i)
    arr[i] = 42;

解决方案 2:地址和迭代器

for (unsigned i = 0; i < len; ++i)
{   *arr = 42;
    ++arr;
}

解决方案3:反向地址和迭代器

for (unsigned i = len; i; --i)
{    *arr = 42;
     ++arr;
}

解决方案4:解决疯狂问题

int* end = arr + len;
for (; arr < end; ++arr)
    *arr = 42;

几乎总是使用明显的解决方案,但我想知道下标运算符是否会导致乘法指令,就好像它写得像*(arr + i * sizeof(int)) = 42一样.

反向解决方案试图利用i0而不是len进行比较如何减轻减法运算。因此,我更喜欢解决方案 3 而不是解决方案 2。此外,我读到数组经过优化,可以转发访问,因为它们在缓存中的存储方式,这可能会给解决方案 1 带来问题。

我不明白为什么解决方案 4 的效率会低于解决方案 2。解决方案 2 递增地址和迭代器,而解决方案 4 仅递增地址。

最后,我不确定我更喜欢哪种解决方案。我认为答案也因编译器的目标架构和优化设置而异。

如果有的话,您更喜欢其中哪一个?

只需使用 std::fill .

std::fill(arr, arr + len, 42);

在您提出的解决方案中,在一个好的编译器上,两者都不应该比其他解决方案更快。

ISO标准没有规定代码中不同方式的效率(除了某些集合算法的某些大O类型的东西),它只是规定了它的功能。

除非你的数组有数十亿个元素的大小,或者你想每分钟设置它们数百万次,否则你使用哪种方法通常不会有丝毫区别。

如果你真的想知道(我仍然认为这几乎肯定是不必要的),你应该对目标环境中的各种方法进行基准测试。测量,不要猜测!

至于我更喜欢哪个,的第一个倾向是优化可读性。只有当存在特定的性能问题时,我才会考虑其他可能性。那只是这样:

for (size_t idx = 0; idx < len; idx++)
    arr[idx] = 42;

我不认为性能在这里是一个问题 - 如果有的话(我可以想象编译器为大多数生成相同的程序集),微优化几乎没有必要。

选择最具可读性的解决方案;标准库为您提供std::fill,或用于更复杂的任务

for(unsigned k = 0; k < len; ++k)
{
    // whatever
}

因此,对于查看您的代码的其他人来说,您在做什么是显而易见的。使用 C++11,您还可以

for(auto & elem : arr)
{
    // whatever
}

只是不要试图在没有任何必要的情况下混淆你的代码。

对于几乎所有有意义的情况,编译器会将所有建议的情况优化为同一件事,并且不太可能产生任何差异。

曾经有一个技巧,如果你向后运行循环,你可以避免自动预取数据,这在某些奇怪的情况下实际上使它更有效率。我不记得确切的情况,但我希望现代处理器无论如何都会识别向后循环以及用于自动预取的前向循环。

如果应用程序对大量元素执行此操作非常重要,那么查看阻塞访问并使用非临时存储将是最有效的。但在执行此操作之前,请确保已将数组的填充确定为重要的性能点,然后对当前代码和改进的代码进行测量。

我可能会回来一些实际的基准来证明"它没有什么区别",但我有一项差事要跑,以免一天太晚......