复制范围中的优化

Optimizations in copying a range

本文关键字:优化 范围 复制      更新时间:2023-10-16

在阅读GNU C++标准库的源代码时,我发现一些代码用于复制(或移动,如果可能的话)一系列迭代器(文件stl_algobase.h),它使用模板专用化进行一些优化。与之相对应的评论写道:

所有这些辅助结构都有两个目的。(1) 尽可能用memmove替换对复制的调用。(Memmove,而不是memcpy,因为允许输入和输出范围重叠。)(2)如果我们使用随机访问迭代器,那么将循环写为带有显式计数的for循环。

使用第二个优化的专业化如下所示:

template<>
struct __copy_move<false, false, random_access_iterator_tag>
{
  template<typename _II, typename _OI>
    static _OI
    __copy_m(_II __first, _II __last, _OI __result)
    { 
  typedef typename iterator_traits<_II>::difference_type _Distance;
  for(_Distance __n = __last - __first; __n > 0; --__n)
    {
      *__result = *__first;
      ++__first;
      ++__result;
    }
  return __result;
  }
};

因此,我有两个关于的问题

  • memmove如何提高复制的速度?它的实现是否比简单的循环更有效
  • for循环中使用显式计数器会如何影响性能

一些澄清:我希望看到编译器实际使用的一些优化示例,而不是详细说明这些示例的可能性。

编辑:第一个问题在这里得到了很好的回答。

在回答第二个问题时,显式计数确实会为循环展开带来更多机会,尽管即使指针在固定大小的数组中迭代,gcc也不会执行主动展开,除非-funroll-loops要求这样做。另一个好处来自于针对非平凡迭代器的可能更简单的循环结束比较测试。

在Core i7-4770上,我通过while循环和显式计数复制实现,对执行最大对齐2048长整数阵列的复制所花费的时间进行了基准测试。(以微秒为单位的时间,包括呼叫开销;带预热的定时环路至少有200个样本。)

                                            while      count
gcc -O3                                     0.179      0.178
gcc -O3 -march=native                       0.097      0.095
gcc -O3 -march=native -funroll-loops        0.066      0.066

在每种情况下,生成的代码都非常相似;在每种情况下,while版本在最后都会做更多的工作,处理检查是否没有任何未填满整个128位(SSE)或256位(AVX)寄存器的条目可以复制,但这些都由分支预测器负责。每个的gcc -O3程序集如下(省略汇编指令)。while版本:

array_copy_while(int (&) [2048], int (&) [2048]):
        leaq    8192(%rdi), %rax
        leaq    4(%rdi), %rdx
        movq    %rax, %rcx
        subq    %rdx, %rcx
        movq    %rcx, %rdx
        shrq    $2, %rdx
        leaq    1(%rdx), %r8
        cmpq    $8, %r8
        jbe     .L11
        leaq    16(%rsi), %rdx
        cmpq    %rdx, %rdi
        leaq    16(%rdi), %rdx
        setae   %cl
        cmpq    %rdx, %rsi
        setae   %dl
        orb     %dl, %cl
        je      .L11
        movq    %r8, %r9
        xorl    %edx, %edx
        xorl    %ecx, %ecx
        shrq    $2, %r9
        leaq    0(,%r9,4), %r10
.L9:
        movdqa  (%rdi,%rdx), %xmm0
        addq    $1, %rcx
        movdqa  %xmm0, (%rsi,%rdx)
        addq    $16, %rdx
        cmpq    %rcx, %r9
        ja      .L9
        leaq    0(,%r10,4), %rdx
        addq    %rdx, %rdi
        addq    %rdx, %rsi
        cmpq    %r10, %r8
        je      .L1
        movl    (%rdi), %edx
        movl    %edx, (%rsi)
        leaq    4(%rdi), %rdx
        cmpq    %rdx, %rax
        je      .L1
        movl    4(%rdi), %edx
        movl    %edx, 4(%rsi)
        leaq    8(%rdi), %rdx
        cmpq    %rdx, %rax
        je      .L20
        movl    8(%rdi), %eax
        movl    %eax, 8(%rsi)
        ret
.L11:
        movl    (%rdi), %edx
        addq    $4, %rdi
        addq    $4, %rsi
        movl    %edx, -4(%rsi)
        cmpq    %rdi, %rax
        jne     .L11
.L1:
        rep ret
.L20:
        rep ret

count版本:

array_copy_count(int (&) [2048], int (&) [2048]):
        leaq    16(%rsi), %rax
        movl    $2048, %ecx
        cmpq    %rax, %rdi
        leaq    16(%rdi), %rax
        setae   %dl
        cmpq    %rax, %rsi
        setae   %al
        orb     %al, %dl
        je      .L23
        movw    $512, %cx
        xorl    %eax, %eax
        xorl    %edx, %edx
.L29:
        movdqa  (%rdi,%rax), %xmm0
        addq    $1, %rdx
        movdqa  %xmm0, (%rsi,%rax)
        addq    $16, %rax
        cmpq    %rdx, %rcx
        ja      .L29
        rep ret
.L23:
        xorl    %eax, %eax
.L31:
        movl    (%rdi,%rax,4), %edx
        movl    %edx, (%rsi,%rax,4)
        addq    $1, %rax
        cmpq    %rax, %rcx
        jne     .L31
        rep ret

然而,当迭代器更加复杂时,差异会变得更加明显。考虑一个假设的容器,它将值存储在一系列固定大小的已分配缓冲区中。迭代器包括指向块链的指针、块索引和块偏移。比较两个迭代器可能需要进行两次比较。增加迭代器需要检查我们是否弹出块边界。

我制作了这样一个容器,并执行了相同的基准测试来复制一个2000长的int容器,其块大小为512 ints。

                                            while      count
gcc -O3                                     1.560      2.818
gcc -O3 -march=native                       1.660      2.854
gcc -O3 -march=native -funroll-loops        1.432      2.858

这看起来很奇怪!哦,等等,这是因为gcc 4.8有一个错误的优化,它使用了条件移动,而不是友好的分支预测器比较。(gcc错误56309)。

让我们在另一台机器(Xeon E5-2670)上尝试icc。

                                            while      count
icc -O3                                     3.952      3.704
icc -O3 -xHost                              3.898      3.624

这更接近于我们的预期,与更简单的循环条件相比,有一个微小但显著的改进。在不同的体系结构上,增益更加明显。clang瞄准1.6GHz的PowerA2:

                                            while      count
bgclang -O3                                36.528     31.623

我将省略这个集会,因为它很长!