Do iota、generate和手卷循环的执行都是一样的

Do iota, generate, and a hand rolled loop all perform the same?

本文关键字:一样 执行 Do generate 循环 iota      更新时间:2023-10-16

这三种填充向量的方式在性能上有区别吗?

#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;
}

我知道它们都产生相同的结果,我感兴趣的只是知道对于较大的向量是否存在速度差异。对于不同的类型,答案会不同吗?

我们可以查看输出程序集(我使用的是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种情况,我不会复制这里的输出,因为它没有带来太多的答案,但问题是编译器真的非常擅长优化这类代码。

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

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

您忘了提到另外一个标准算法——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

也许读一下我关于算法std::iota的建议会很有趣,虽然遗传文本是用俄语写的,你将能够使用例如谷歌服务翻译来阅读它。