最有效地添加编译时大小的数组的所有元素

时间:2015-09-15 02:03:52

标签: c++ templates recursion

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

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

这似乎可以让我最有效地完成工作。我认为除了添加之外的分支和算法将被完全优化。这样做是否有任何缺陷?

3 个答案:

答案 0 :(得分:4)

不要试图超越优化器。所有这些复杂的模板机制只会让优化器更难理解你真正想做的事情。

例如,

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

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

在-O3

处生成带有clang的exact same assembly
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产生类似的结果。

答案 1 :(得分:1)

这里重要的限定符是&#34;最少数量的指令&#34;的含义。如果这被解释为导致CPU执行最少的步骤,并且我们进一步规定没有先进的技术可用,例如SIMD,GPU编程或OMP(或其他自动并行技术)....只是C或C ++,然后考虑:

假设有类似的东西:

int a[ 10 ];

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

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

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

当然,有些const int表示数组的大小&#39; a&#39;会有秩序的。

这奇怪地与:

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

编译器非常巧妙地发出了10个已展开的添加指令 - 它甚至不会打扰循环。

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

另一方面,for循环的展开结果只有10个机器步骤,CPU很可能安排好缓存友好性,并且没有跳转。

for循环肯定更快。

编译器&#34;知道&#34;你正在尝试做什么,并且可以通过你发布的建议代码来考虑你的工作。

此外,如果数组的大小对于展开循环而言过于古怪,编译器会自动执行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的发布版本中生成以下汇编程序片段

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]  

基于评论我决定在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中并行运行,因为之前的条目正在完成它&#39此外,与VC的简单,干净,外观相比,成本几乎为零。系列增加内存源。

以下是Xcode的std :: accumulator。它看起来需要初始化,但是它执行了一系列清晰的添加,展开了循环,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 - 但是这个简短的调查无法揭示这是否是libary或编译器实现的差异。在Xcode的std::accumulate的实现中可能存在重要的差异,这使得编译器比VC的库版本更具洞察力。

这更普遍适用于算法,甚至是数字算法。 std::accumulate是for循环。它可能基于指向数组的指针扩展内联,这就是为什么VC为std :: accumulate创建循环的选择在它为使用代码生成循环的选择中得到了回应。 int *遍历数组,但是使用整数展开for循环的循环,以通过索引引用数组中的条目。换句话说,当使用指针时,它在直接循环中确实没有做得更好,这表明它是VC的优化器,而不是库,在这种情况下。

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

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

糟糕的优化器可能会忽略这一点,并且库的不良实现可能会使优化器混淆,这意味着在最佳情况下std::accumulate可以执行与for循环一样的简单整数,生成循环的展开版本创建总和,但并非总是如此。但是,在for循环中,编译器的理解很少受到影响......一切都在那里,而且库的实现不会弄乱它,它会导致它们崩溃,#39 ;那时所有由编译器决定。为此,VC显示了它的弱点。

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

让Xcode展开循环并没有多大帮助; LLVM似乎有更深入的工程。它也可能有更好的库实现。

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

答案 2 :(得分:0)

虽然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中,带有fold表达式:

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