【问题标题】:Add up all elements of compile-time sized array most efficiently最有效地将编译时大小的数组的所有元素相加
【发布时间】:2017-08-22 17:52:55
【问题描述】:

我正在尝试使用最少的指令有效地将所有内容添加到一个编译时大小的数组中。当然,我正在使用模板。这是我创建的。

template<unsigned int startIndex, unsigned int count>
int AddCollapseArray(int theArray[])
{
    if(count == 1)
    {
        return theArray[startIndex];
    }
    else if(count == 2)
    {
        return theArray[startIndex] + theArray[startIndex + 1];
    }
    else if(count % 2 == 0)
    {
        return AddCollapseArray<startIndex, count / 2>(theArray) + AddCollapseArray<startIndex + count / 2, count / 2>(theArray));
    }
    else if (count % 2 == 1)
    {
        int newCount = count-1;
        return AddCollapseArray<startIndex, newCount/ 2>(theArray) + AddCollapseArray<startIndex + newCount/ 2, newCount/ 2>(theArray)) + theArray[startIndex + newCount];
    }
}

这对我来说似乎可以最有效地完成工作。我认为除了加法之外的分支和算术将被完全优化。这样做有什么缺陷吗?

【问题讨论】:

  • 您必须这样做有什么特殊原因吗?我会使用 std::accumulate 并保持原样,除非有理由不这样做。
  • 如果将 10 个数字相加,则需要 9 次加法运算。没有办法解决这个问题。如果你为 10 个元素制定递归方案,你会发现它确实会使用 9 个加法。
  • 当我阅读使用“最少指令数”的规定时,我在考虑机器语言指令或 C 或 C++ 中最接近的等价物,这是分配吗?此外,您声明数组的大小在编译时是已知的,但数组中的 VALUES 在编译时是否也是已知的?在后者中,您可以在编译期间使用简单的元编程进行计算,从而在运行时产生零指令,但前提是在编译时知道 VALUES。
  • @JVene 值在编译时未知。
  • 仅使用std::accumulate 会在clang 上产生identical codegen,在GCC 上可能会产生better codegen 10 个元素。此外,随着元素数量的增加,bothcompilers 可以向量化 accumulate,但不能向量化您的函数。

标签: c++ templates recursion


【解决方案1】:

不要试图智取优化器。所有这些复杂的模板机制只会让优化器更难理解你真正想要做什么。

例如,

int f0(int *p) {
  return AddCollapseArray<0, 10>(p);
}

int f1(int *p) {
  return std::accumulate(p+0, p+10, 0);
}

生成exact same assembly,并在 -O3 处发出叮当声

f0(int*):                                # @f0(int*)
    movl    4(%rdi), %eax
    addl    (%rdi), %eax
    addl    8(%rdi), %eax
    addl    12(%rdi), %eax
    addl    16(%rdi), %eax
    addl    20(%rdi), %eax
    addl    24(%rdi), %eax
    addl    28(%rdi), %eax
    addl    32(%rdi), %eax
    addl    36(%rdi), %eax
    retq

f1(int*):                                # @f1(int*)
    movl    4(%rdi), %eax
    addl    (%rdi), %eax
    addl    8(%rdi), %eax
    addl    12(%rdi), %eax
    addl    16(%rdi), %eax
    addl    20(%rdi), %eax
    addl    24(%rdi), %eax
    addl    28(%rdi), %eax
    addl    32(%rdi), %eax
    addl    36(%rdi), %eax
    retq

假设我们要做 100 个元素:

int f0(int *p) {
  return AddCollapseArray<0, 100>(p);
}

int f1(int *p) {
  return std::accumulate(p+0, p+100, 0);
}

Here's what we get:

f0(int*):                                # @f0(int*)
    pushq   %rbp
    pushq   %rbx
    pushq   %rax
    movq    %rdi, %rbx
    callq   int AddCollapseArray<0u, 50u>(int*)
    movl    %eax, %ebp
    movq    %rbx, %rdi
    callq   int AddCollapseArray<50u, 50u>(int*)
    addl    %ebp, %eax
    addq    $8, %rsp
    popq    %rbx
    popq    %rbp
    retq

f1(int*):                                # @f1(int*)
    movdqu  (%rdi), %xmm0
    movdqu  16(%rdi), %xmm1
    movdqu  32(%rdi), %xmm2
    movdqu  48(%rdi), %xmm3
    paddd   %xmm0, %xmm1
    paddd   %xmm2, %xmm1
    paddd   %xmm3, %xmm1
    movdqu  64(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  80(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  96(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  112(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  128(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  144(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  160(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  176(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  192(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  208(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  224(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  240(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  256(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  272(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  288(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  304(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  320(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  336(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  352(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    movdqu  368(%rdi), %xmm1
    paddd   %xmm0, %xmm1
    movdqu  384(%rdi), %xmm0
    paddd   %xmm1, %xmm0
    pshufd  $78, %xmm0, %xmm1       # xmm1 = xmm0[2,3,0,1]
    paddd   %xmm0, %xmm1
    pshufd  $229, %xmm1, %xmm0      # xmm0 = xmm1[1,1,2,3]
    paddd   %xmm1, %xmm0
    movd    %xmm0, %eax
    retq

int AddCollapseArray<0u, 50u>(int*):     # @int AddCollapseArray<0u, 50u>(int*)
    movl    4(%rdi), %eax
    addl    (%rdi), %eax
    addl    8(%rdi), %eax
    addl    12(%rdi), %eax
    addl    16(%rdi), %eax
    addl    20(%rdi), %eax
    addl    24(%rdi), %eax
    addl    28(%rdi), %eax
    addl    32(%rdi), %eax
    addl    36(%rdi), %eax
    addl    40(%rdi), %eax
    addl    44(%rdi), %eax
    addl    48(%rdi), %eax
    addl    52(%rdi), %eax
    addl    56(%rdi), %eax
    addl    60(%rdi), %eax
    addl    64(%rdi), %eax
    addl    68(%rdi), %eax
    addl    72(%rdi), %eax
    addl    76(%rdi), %eax
    addl    80(%rdi), %eax
    addl    84(%rdi), %eax
    addl    88(%rdi), %eax
    addl    92(%rdi), %eax
    addl    96(%rdi), %eax
    addl    100(%rdi), %eax
    addl    104(%rdi), %eax
    addl    108(%rdi), %eax
    addl    112(%rdi), %eax
    addl    116(%rdi), %eax
    addl    120(%rdi), %eax
    addl    124(%rdi), %eax
    addl    128(%rdi), %eax
    addl    132(%rdi), %eax
    addl    136(%rdi), %eax
    addl    140(%rdi), %eax
    addl    144(%rdi), %eax
    addl    148(%rdi), %eax
    addl    152(%rdi), %eax
    addl    156(%rdi), %eax
    addl    160(%rdi), %eax
    addl    164(%rdi), %eax
    addl    168(%rdi), %eax
    addl    172(%rdi), %eax
    addl    176(%rdi), %eax
    addl    180(%rdi), %eax
    addl    184(%rdi), %eax
    addl    188(%rdi), %eax
    addl    192(%rdi), %eax
    addl    196(%rdi), %eax
    retq

int AddCollapseArray<50u, 50u>(int*):    # @int AddCollapseArray<50u, 50u>(int*)
    movl    204(%rdi), %eax
    addl    200(%rdi), %eax
    addl    208(%rdi), %eax
    addl    212(%rdi), %eax
    addl    216(%rdi), %eax
    addl    220(%rdi), %eax
    addl    224(%rdi), %eax
    addl    228(%rdi), %eax
    addl    232(%rdi), %eax
    addl    236(%rdi), %eax
    addl    240(%rdi), %eax
    addl    244(%rdi), %eax
    addl    248(%rdi), %eax
    addl    252(%rdi), %eax
    addl    256(%rdi), %eax
    addl    260(%rdi), %eax
    addl    264(%rdi), %eax
    addl    268(%rdi), %eax
    addl    272(%rdi), %eax
    addl    276(%rdi), %eax
    addl    280(%rdi), %eax
    addl    284(%rdi), %eax
    addl    288(%rdi), %eax
    addl    292(%rdi), %eax
    addl    296(%rdi), %eax
    addl    300(%rdi), %eax
    addl    304(%rdi), %eax
    addl    308(%rdi), %eax
    addl    312(%rdi), %eax
    addl    316(%rdi), %eax
    addl    320(%rdi), %eax
    addl    324(%rdi), %eax
    addl    328(%rdi), %eax
    addl    332(%rdi), %eax
    addl    336(%rdi), %eax
    addl    340(%rdi), %eax
    addl    344(%rdi), %eax
    addl    348(%rdi), %eax
    addl    352(%rdi), %eax
    addl    356(%rdi), %eax
    addl    360(%rdi), %eax
    addl    364(%rdi), %eax
    addl    368(%rdi), %eax
    addl    372(%rdi), %eax
    addl    376(%rdi), %eax
    addl    380(%rdi), %eax
    addl    384(%rdi), %eax
    addl    388(%rdi), %eax
    addl    392(%rdi), %eax
    addl    396(%rdi), %eax
    retq

您的函数不仅没有完全内联,而且也没有向量化。 GCC 产生类似的结果。

【讨论】:

    【解决方案2】:

    这里重要的限定词是“最少指令数”的意思。如果这被解释为使 CPU 执行最少的步骤,并且我们进一步规定不使用高级技术,如 SIMD、GPU 编程或 OMP(或其他自动并行技术)......只是 C 或C++,然后考虑:

    假设如下:

    int a[ 10 ];
    

    在运行时填充数据,并且始终包含 10 个条目(0 到 9)

    std::accumulate 在这里做得很好,在汇编程序中创建了一个紧密的循环,没有混乱......只是快速:

    int r = std::accumulate( &a[ 0 ], &a[ 9 ], 0 );
    

    如果当然,一些表示数组“a”大小的 const int 将是有序的。

    这比较奇怪:

    for( int n=0; n < 10; ++n ) r += a[ n ];
    

    编译器非常巧妙地发出 10 条展开的添加指令 - 它甚至不用担心循环。

    现在,这意味着在std::accumulate 中,尽管循环很紧凑,但每个元素至少会有两条加法指令(一条用于求和,一条用于递增迭代器)。加上比较指令和条件跳转,每项至少有 4 条指令,或大约 40 个不同成本的机器语言步骤。

    另一方面,for 循环的展开结果只有 10 个机器步骤,CPU 很可能会以非常友好的缓存进行调度,并且没有跳转。

    for 循环肯定更快。

    编译器“知道”您正在尝试做什么,并且可以使用您发布的建议代码来完成这项工作。

    此外,如果数组的大小变得太古怪而无法展开循环,编译器会自动执行 std::accumulate 由于某种原因似乎没有执行的经典优化...即,每个循环执行两次加法(当由于元素的数量,它构造了一个循环)。

    使用 VC 2012,此来源:

     int r = std::accumulate( &a[ 0 ], &a[ 9 ], 0 );
    
     int z = 0;
    
     int *ap = a;
     int *ae = &a[9];
     while( ap <= ae ) { z += *ap; ++ap; }
    
     int z2 = 0;
    
     for (int n=0; n < 10; ++n ) z2 += a[ n ];
    

    在 VC2012 的发布版本中生成以下汇编器 sn-ps

    int r = std::accumulate( &a[ 0 ], &a[ 9 ], 0 );
    00301270 33 D2                xor         edx,edx  
    00301272 B8 D4 40 30 00       mov         eax,3040D4h  
    00301277 EB 07                jmp         wmain+10h (0301280h)  
    00301279 8D A4 24 00 00 00 00 lea         esp,[esp]  
    00301280 03 10                add         edx,dword ptr [eax]  
    00301282 83 C0 04             add         eax,4  
    00301285 3D F8 40 30 00       cmp         eax,3040F8h  
    0030128A 75 F4                jne         wmain+10h (0301280h) 
    
    while( ap <= ae ) { z += *ap; ++ap; }
    003012A0 03 08                add         ecx,dword ptr [eax]  
    003012A2 03 70 04             add         esi,dword ptr [eax+4]  
    003012A5 83 C0 08             add         eax,8  
    003012A8 3D F4 40 30 00       cmp         eax,3040F4h  
    003012AD 7E F1                jle         wmain+30h (03012A0h)  
    003012AF 3D F8 40 30 00       cmp         eax,3040F8h  
    003012B4 77 02                ja          wmain+48h (03012B8h)  
    003012B6 8B 38                mov         edi,dword ptr [eax]  
    003012B8 8D 04 0E             lea         eax,[esi+ecx]  
    003012BB 03 F8                add         edi,eax  
    
    
    for (int n=0; n < 10; ++n ) z2 += a[ n ];
    003012BD A1 D4 40 30 00       mov         eax,dword ptr ds:[003040D4h]  
    003012C2 03 05 F8 40 30 00    add         eax,dword ptr ds:[3040F8h]  
    003012C8 03 05 D8 40 30 00    add         eax,dword ptr ds:[3040D8h]  
    003012CE 03 05 DC 40 30 00    add         eax,dword ptr ds:[3040DCh]  
    003012D4 03 05 E0 40 30 00    add         eax,dword ptr ds:[3040E0h]  
    003012DA 03 05 E4 40 30 00    add         eax,dword ptr ds:[3040E4h]  
    003012E0 03 05 E8 40 30 00    add         eax,dword ptr ds:[3040E8h]  
    003012E6 03 05 EC 40 30 00    add         eax,dword ptr ds:[3040ECh]  
    003012EC 03 05 F0 40 30 00    add         eax,dword ptr ds:[3040F0h]  
    003012F2 03 05 F4 40 30 00    add         eax,dword ptr ds:[3040F4h]  
    

    基于 cmets,我决定在 XCode 7 中尝试这个,结果截然不同。这是 for 循环的展开:

        .loc    1 58 36                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:36
        movq    _a(%rip), %rax
    Ltmp22:
        ##DEBUG_VALUE: do3:z2 <- EAX
        movq    %rax, %rcx
        shrq    $32, %rcx
        .loc    1 58 33 is_stmt 0       ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:33
        addl    %eax, %ecx
        .loc    1 58 36                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:36
        movq    _a+8(%rip), %rax
    Ltmp23:
        .loc    1 58 33                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:33
        movl    %eax, %edx
        addl    %ecx, %edx
        shrq    $32, %rax
        addl    %edx, %eax
        .loc    1 58 36                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:36
        movq    _a+16(%rip), %rcx
        .loc    1 58 33                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:33
        movl    %ecx, %edx
        addl    %eax, %edx
        shrq    $32, %rcx
        addl    %edx, %ecx
        .loc    1 58 36                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:36
        movq    _a+24(%rip), %rax
        .loc    1 58 33                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:33
        movl    %eax, %edx
        addl    %ecx, %edx
        shrq    $32, %rax
        addl    %edx, %eax
        .loc    1 58 36                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:36
        movq    _a+32(%rip), %rcx
        .loc    1 58 33                 ## /Users/jv/testclang/testcp/checkloop/checkloop/main.cpp:58:33
        movl    %ecx, %edx
        addl    %eax, %edx
        shrq    $32, %rcx
        addl    %edx, %ecx
    

    这可能看起来不像 VC 的简单列表那样干净,但它可能运行得一样快,因为每个添加的设置(movq 或 movl)可能在 CPU 中并行运行,因为前一个条目正在完成它的添加,几乎没有成本与 VC 简单、干净的“外观”系列添加内存源相比。

    以下是 Xcode 的 std::accumulator。它似乎需要一个 init,但随后它执行了一系列干净的添加并展开了循环,而 VC 没有这样做。

        .file   37 "/Applications/Xcode7.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1" "numeric"
        .loc    37 75 27 is_stmt 1      ## /Applications/Xcode7.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/numeric:75:27
        movq    _a(%rip), %r14
    Ltmp11:
        movq    %r14, -48(%rbp)         ## 8-byte Spill
    Ltmp12:
        shrq    $32, %r14
        movq    _a+8(%rip), %rbx
        movq    %rbx, -56(%rbp)         ## 8-byte Spill
        shrq    $32, %rbx
        movq    _a+16(%rip), %r13
        movq    %r13, -72(%rbp)         ## 8-byte Spill
        shrq    $32, %r13
        movq    _a+24(%rip), %r15
        movq    %r15, %r12
        shrq    $32, %r12
    Ltmp13:
        movl    _a+32(%rip), %eax
    Ltmp14:
        movq    -48(%rbp), %rax         ## 8-byte Reload
        addl    %eax, %r14d
        movq    -56(%rbp), %rax         ## 8-byte Reload
        addl    %eax, %r14d
        addl    %ebx, %r14d
        movq    -72(%rbp), %rax         ## 8-byte Reload
        addl    %eax, %r14d
        addl    %r13d, %r14d
        addl    %r15d, %r14d
        addl    %r12d, %r14d
        addl    -64(%rbp), %r14d        ## 4-byte Folded Reload
    

    这里的底线是,我们依赖于编译器的优化在一个编译器之间存在如此广泛和广泛的差异,我们应该依赖它们,但要注意。

    LLVM 堪称典范,似乎比 VC 更能理解 std::accumulate - 但这个简短的调查无法揭示这是否是库或编译器实现的差异。 Xcode 的std::accumulate 的实现可能存在重要差异,这使编译器比 VC 版本的库更深入。

    这更普遍地适用于算法,即使是数字算法。 std::accumulate 是一个 for 循环。它可能会根据指向数组的指针将内联扩展为 for 循环,这就是为什么 VC 选择为 std::accumulate 创建循环的原因与它选择使用int * 为代码生成循环以循环数组的原因相呼应,但使用整数展开 for 循环的循环,以按索引引用数组中的条目。换句话说,当使用指针时,它在直接 for 循环中确实没有做得更好,这表明在这种情况下它是 VC 的优化器,而不是库。

    这遵循了 Stroustrup 自己最喜欢的关于编译器可用信息概念的示例,比较了 C 中的 qsort 和 C++ 中的 sort。 qsort 采用函数指针来执行比较,切断编译器对比较的理解,强制它通过指针调用函数。另一方面,C++ sort 函数采用函子,它传达了有关比较的更多信息。这仍然可能导致函数调用,但优化器有机会充分理解比较以使其内联。

    在 VC 的情况下,无论出于何种原因(我们必须像微软一样),编译器在通过指针循环遍历数组时会感到困惑。提供给它的信息与使用整数索引数组的循环不同。它明白这一点,但不是指针。相比之下,LLVM 理解两者(以及更多)。信息的差异对 LLVM 并不重要,但对 VC 很重要。由于std::accumulate 实际上是一个表示for 循环的内联,并且该循环是通过指针处理的,因此它逃脱了VC 的识别,就像VC 在基于指针的直接for 循环中所做的那样。如果可以对整数数组进行专门化,这样累积循环使用索引而不是指针,VC 会以更好的输出响应,但它不应该如此。

    一个糟糕的优化器可能会错过重点,一个糟糕的库实现可能会使优化器感到困惑,这意味着在最好的情况下std::accumulate 可以像 for 循环一样执行简单的整数数组,产生创建总和的循环的展开版本,但并非总是如此。但是,在 for 循环中几乎没有什么可以妨碍编译器的理解......一切都在那里,库的实现不能把它搞砸,这一切都取决于编译器。对此,VC显示了它的弱点。

    我尝试了 VC 上的所有设置,试图让它展开 std::accumulate,但到目前为止它从未这样做过(没有尝试过更新版本的 VC)。

    让 Xcode 展开循环并不需要太多时间; LLVM 似乎有更深层次的工程。它也可能有更好的库实现。

    顺便说一句,我在顶部发布的 C 代码示例是在 VC 中使用的,它没有认识到这三个不同的求和是相关的。 XCode 上的 LLVM 确实如此,这意味着我第一次在那里尝试它时,它只是采用了 std::accumulate 的答案,否则什么也没做。 VC在这一点上真的很弱。为了让 Xcode 执行 3 次单独的测试,我在每次调用之前随机化了数组……否则 Xcode 会意识到我在做什么,而 VC 没有。

    【讨论】:

    • 我觉得自己需要复杂的递归而不是展开的 for 循环,这让我感到很愚蠢
    • 请不要,我认为这可能是分配的原因。我们曾经有愚蠢的编译器……在我年轻的时候,具有 4 MB RAM 的机器是巨大的(这不是印刷错误,是兆字节)。他们无法像今天那样优化......我们已经学会了信任编译器,以至于不打扰汇编器或试图超越它。更重要的是,像 std::accumulate 这样的便利是好的,尽可能快,但真正重要的一件事是编译器从我们的代码上下文中收集了多少信息。如果该上下文丢失,优化将丢失。
    • 为什么你认为编译器不能看穿std::accumulate中的迭代器?
    • 这可能是特定于实现的,但基本要点是:它使用迭代器。这与使用 int * 循环遍历数组相比。那时,编译器失去了上下文,并没有意识到你的意图。它与累加器对指针的使用一起使用,而是创建了一个循环。写一个 int * 并在 while 或 for 中循环到数组的末尾也会发生同样的事情。
    • 我不知道您使用的是哪种古代编译器。自 4.4(godbolt 上可用的最旧版本)以来没有 GCC 为 int f(int* p) { return std::accumulate(p, p+10, 0); } 发出循环。
    【解决方案3】:

    std::accumulate 应该足够了,手动展开循环,你可以这样做

    namespace detail
    {
        template<std::size_t startIndex, std::size_t... Is>
        int Accumulate(std::index_sequence<Is...>, const int a[])
        {
            int res = 0;
            const int dummy[] = {0, ((res += a[startIndex + Is]), 0)...};
            static_cast<void>(dummy); // Remove warning for unused variable
            return res;
        }
    }
    
    template<std::size_t startIndex, std::size_t count>
    int AddCollapseArray(const int a[])
    {
        return detail::Accumulate<startIndex>(std::make_index_sequence<count>{}, a);
    }
    

    或在 C++17 中,使用折叠表达式:

    namespace detail
    {
        template<std::size_t startIndex, std::size_t... Is>
        int Accumulate(std::index_sequence<Is...>, const int a[])
        {
            return (a[startIndex + Is] + ...);
        }
    }
    

    【讨论】:

    • 这与矢量化器有同样的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-01
    • 1970-01-01
    • 2021-05-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多