我正在尝试使用最少量的指令在编译时大小的数组中有效地添加所有内容。当然我正在使用模板。我创造了这个。
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];
}
}
这似乎可以让我最有效地完成工作。我认为除了添加之外的分支和算法将被完全优化。这样做是否有任何缺陷?
答案 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 assemblyf0(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);
}
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] + ...);
}
}