iota、generate 和 hand rolling loop 的性能都一样吗?

Posted

技术标签:

【中文标题】iota、generate 和 hand rolling loop 的性能都一样吗?【英文标题】:Do iota, generate, and a hand rolled loop all perform the same? 【发布时间】:2014-08-24 19:47:22 【问题描述】:

这三种填充向量的方式是否存在性能差异?

#include <vector>
#include <numeric>
#include <algorithm>
#include <iterator>

int main()

    std::vector<int> v(10);
    std::iota(v.begin(), v.end(), 0);

    std::vector<int> v2(10);
    int i = 0;
    std::generate(v2.begin(), v2.end(), [&i]()return i++; );

    std::vector<int> v3(10);
    i = 0;
    for (auto& j : v3)
    
        j = i++;
    

return 0;

我知道它们都产生相同的结果,我只想知道更大的向量是否存在速度差异。 不同类型的答案会有所不同吗?

【问题讨论】:

@WhozCraig 他们不都重视初始化 10 个元素吗? @juanchopanza 实际上,是的,他们确实如此,现在我盯着他们看。我将有reserve()ed 并回插入generate_n,这让我陷入了后者的低效率。你是绝对正确的。真的需要在星期天停止阅读。谢谢你让我诚实。删除先前的评论。 无论如何,我在标准中找不到任何可以为这三个变体中的任何一个带来性能优势的东西。如果有,它将取决于实现。当然,对于用户定义的类型,使用预增量可能会给iota 带来优势。 @WhozCraig:我认为reserve()std::back_inserter() 可以做得很好,但实际上它似乎只为非常大的向量带来了回报(在我的Mac上尝试使用最新版本的gcc和clang -O3)。最有可能的是,对容量的检查和对向量大小的吹嘘会扼杀reserve() 方法的性能:他们需要一个定制版本的算法,以一种巧妙的方式避免这些成本。 @DietmarKühl 我也有同样的看法。我主要将它用于具有非默认构造的对象的向量或非常大的向量,无论其类型如何。通常我对这种工件的性能不感兴趣,除非它是一个非常关键算法(也许是这样)。 【参考方案1】:

我们可以查看输出程序集(我使用了gcc.godbolt.org,gcc -03,以及您的代码):

1) 第一个版本,std::iota

main:
    sub rsp, 8
    mov edi, 40
    call    operator new(unsigned long)
    mov DWORD PTR [rax], 0
    mov DWORD PTR [rax+4], 1
    mov rdi, rax
    mov DWORD PTR [rax+8], 2
    mov DWORD PTR [rax+12], 3
    mov DWORD PTR [rax+16], 4
    mov DWORD PTR [rax+20], 5
    mov DWORD PTR [rax+24], 6
    mov DWORD PTR [rax+28], 7
    mov DWORD PTR [rax+32], 8
    mov DWORD PTR [rax+36], 9
    call    operator delete(void*)
    xor eax, eax
    add rsp, 8
    ret

2) 带有 std::generate 和 Lambda 的版本:

main:
    sub rsp, 8
    mov edi, 40
    call    operator new(unsigned long)
    mov DWORD PTR [rax], 0
    mov DWORD PTR [rax+4], 1
    mov rdi, rax
    mov DWORD PTR [rax+8], 2
    mov DWORD PTR [rax+12], 3
    mov DWORD PTR [rax+16], 4
    mov DWORD PTR [rax+20], 5
    mov DWORD PTR [rax+24], 6
    mov DWORD PTR [rax+28], 7
    mov DWORD PTR [rax+32], 8
    mov DWORD PTR [rax+36], 9
    call    operator delete(void*)
    xor eax, eax
    add rsp, 8
    ret

3) 最后一个版本,带有手写循环:

main:
    sub rsp, 8
    mov edi, 40
    call    operator new(unsigned long)
    mov DWORD PTR [rax], 0
    mov DWORD PTR [rax+4], 1
    mov rdi, rax
    mov DWORD PTR [rax+8], 2
    mov DWORD PTR [rax+12], 3
    mov DWORD PTR [rax+16], 4
    mov DWORD PTR [rax+20], 5
    mov DWORD PTR [rax+24], 6
    mov DWORD PTR [rax+28], 7
    mov DWORD PTR [rax+32], 8
    mov DWORD PTR [rax+36], 9
    call    operator delete(void*)
    xor eax, eax
    add rsp, 8
    ret

结论:

正如预期的那样,所有三个都生成相同的程序集(全部展开),并带有一个不错的编译器,启用了优化。

所以不,没有性能差异。


注意:

我进行了将程序集与足够大的向量进行比较而没有展开循环的测试(我不知道 GCC 启发式,但它开始于大小 >~ 15)。

在那种情况下,对于所有 3 种情况,程序集仍然相同,我不会在此处复制输出,因为它不会带来太多答案,但问题是编译器是真的非常擅长优化这种代码。

【讨论】:

+1 值得注意的是,一个 64 位汇编器和一个体面的优化器会将其压缩到明显更小的 movs(实际上是一半)。 see listing here.【参考方案2】:

当然,找出答案的正确方法是测量和/或比较生成的代码。由于std::vector&lt;T&gt;T 类型的对象使用连续内存,编译器可能会看穿所有3 个版本的循环并生成几乎相同的代码。此外,对于您设置中的特定算法,几乎没有什么智能实现可以做。情况会有所不同,例如,当使用 std::deque&lt;T&gt; 时,算法可以单独处理段以提高性能(我不知道有任何实际这样做的实现)。

如果性能是您最关心的问题并且您正在使用大向量,您可能希望一开始就创建一个大向量,因为这可能会触及所有内存,尽管它即将被覆盖。相反,您将构造并清空向量,reserve() 足够的内存,然后使用合适的目标迭代器(例如,std::back_inserter(v))。不过,这些方法需要适当地改变。在算法中构造对象时,算法实际上可以应用一些智能,而天真的循环使用,例如 push_back()s 或合适的附加迭代器可能不适用:因为算法可以看到他们将创建多少个对象,他们可以将针对容量的检查提升到循环之外(尽管它需要通过迭代器类型进行一些特殊访问)。即使算法中没有优化,我也希望对向量进行单次传递比算法中的任何调整对性能有更大的好处。

【讨论】:

【参考方案3】:

你忘了提到另外一种标准算法——算法std::for_each

例如

std::vector<int> v4(10);
int i = 0;
std::for_each(v4.begin(), v4.end(), [&i]( int &item ) item = i++;  );

算法和基于范围的for语句没有本质区别。事实上,它们相互复制。例如,基于范围的 for 语句使用相同的方法 begin() 和 end()。

所以最好注意表现力。在这种情况下,我更喜欢std::iota

也许阅读my proposal on algorithm std::iota 会很有趣,虽然基因文本是用俄语编写的,但您可以使用例如谷歌服务翻译来阅读它。

【讨论】:

以上是关于iota、generate 和 hand rolling loop 的性能都一样吗?的主要内容,如果未能解决你的问题,请参考以下文章

Go iota 原理和源码剖析

IOTA 交易,确认和共识

8iota枚举

常量和iota

golang初识 和 变量,常量,iota

Can't generate API documentation in l5-swagger