问题:使用排序过的数组,比未排序的数组运行速度要快。作为有探究精神的你,肯定要问为什么?第一反应,它应该和存储有关,当排序后数组被连续的存储在一块连续的内存(应该是内存还是地址块更恰当呢?)中,如图:
那么最后造成速度快慢的原因,应该源自于两种存储方式下,对数据访问的不同所造成。
接下来,老规矩,上代码测试。
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster
std::sort(data, data + arraySize);
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
gcc编译后得到结果,排序后的情况:
未排序的情况:,时间上差了很多。当然了,这只能说明在数据量相当大的情况下,排序对性能有影响。再来测试一个数据量比较小的情况,将代码改为
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 12;
// !!! With this, the next loop runs faster
std::sort(data, data + arraySize);
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 6)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
排序--> 未排序-->
。
在数据量很小的情况下,两者并没有太大的差别。为了提高程序的性能,我们可以适当的使用sort()来优化。
回到主题,将test.cpp编译成汇编代码
LBB0_1: ## => 对应操作 for (unsigned c = 0; c < arraySize; ++c)
callq _rand ## => data[c] = std::rand() % 256;
movl %eax, %ecx
sarl $31, %ecx
shrl $24, %ecx
addl %eax, %ecx
andl $-256, %ecx
subl %ecx, %eax
movl %eax, -131120(%rbp,%rbx,4)
incq %rbx
cmpq $32768, %rbx
jne LBB0_1
## BB#2: ## => std::sort(data, data + arraySize);
leaq -48(%rbp), %rsi ## => clock_t start = clock();
leaq -131120(%rbp), %rdi
leaq -131136(%rbp), %rdx
callq __ZNSt3__16__sortIRNS_6__lessIiiEEPiEEvT0_S5_T_
xorl %ebx, %ebx
callq _clock
movq %rax, %r14
movdqa LCPI0_0(%rip), %xmm8
movdqa LCPI0_1(%rip), %xmm9
pxor %xmm8, %xmm9
xorl %r13d, %r13d
.align 4, 0x90
LBB0_3: ## %overflow.checked
## =>This Loop Header: Depth=1
## Child Loop BB0_4 Depth 2
movd %r13, %xmm3
pxor %xmm2, %xmm2
xorl %eax, %eax
.align 4, 0x90
LBB0_4: ## %vector.body
## Parent Loop BB0_3 Depth=1
## => 内循环:for (unsigned c = 0; c < arraySize; ++c)
## {
## if (data[c] >= 128)
## sum += data[c];
## }
movslq -131116(%rbp,%rax,4), %rcx
movd %rcx, %xmm5
movslq -131120(%rbp,%rax,4), %rcx
movd %rcx, %xmm4
punpcklqdq %xmm5, %xmm4
movslq -131108(%rbp,%rax,4), %rcx
movd %rcx, %xmm6
movslq -131112(%rbp,%rax,4), %rcx
movd %rcx, %xmm5
punpcklqdq %xmm6, %xmm5
movdqa %xmm4, %xmm6
pxor %xmm8, %xmm6
movdqa %xmm6, %xmm7
pcmpgtd %xmm9, %xmm7
pshufd $160, %xmm7, %xmm0
pcmpeqd %xmm9, %xmm6
pshufd $245, %xmm6, %xmm6
pand %xmm0, %xmm6
pshufd $245, %xmm7, %xmm0
por %xmm6, %xmm0
movdqa %xmm5, %xmm6
pxor %xmm8, %xmm6
movdqa %xmm6, %xmm7
pcmpgtd %xmm9, %xmm7
pshufd $160, %xmm7, %xmm1
pcmpeqd %xmm9, %xmm6
pshufd $245, %xmm6, %xmm6
pand %xmm1, %xmm6
pshufd $245, %xmm7, %xmm1
por %xmm6, %xmm1
paddq %xmm3, %xmm4
paddq %xmm2, %xmm5
pand %xmm0, %xmm4
pandn %xmm3, %xmm0
movdqa %xmm0, %xmm3
por %xmm4, %xmm3
pand %xmm1, %xmm5
pandn %xmm2, %xmm1
movdqa %xmm1, %xmm2
por %xmm5, %xmm2
addq $4, %rax
cmpq $32768, %rax ## imm = 0x8000
jne LBB0_4
## BB#5: ## %middle.block
## 外部循环:for (unsigned i = 0; i < 100000; ++i)
paddq %xmm3, %xmm2
pshufd $78, %xmm2, %xmm0
paddq %xmm2, %xmm0
movd %xmm0, %r13
incl %ebx
cmpl $100000, %ebx
jne LBB0_3
## BB#6: static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
callq _clock
subq %r14, %rax
movd %rax, %xmm0
punpckldq LCPI0_2(%rip), %xmm0 ## xmm0 = xmm0[0],mem[0],xmm0[1],mem[1]
subpd LCPI0_3(%rip), %xmm0
haddpd %xmm0, %xmm0
divsd LCPI0_4(%rip), %xmm0
movq __ZNSt3__14coutE@GOTPCREL(%rip), %rdi
callq __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEd
movq %rax, %r14
movq (%r14), %rax
movq -24(%rax), %rsi
addq %r14, %rsi
leaq -131136(%rbp), %r15
movq %r15, %rdi
callq __ZNKSt3__18ios_base6getlocEv
发现从汇编代码,并不能看出什么,两者完全一样。那么这个问题就不是汇编层级的优化导致的。然后在查找了一些资料后发现,是CPU流水指令有一个分支预测技术。
代码中内循环里有一个条件判断。每一次CPU执行这个条件判断时,CPU都可能跳转到循环开始处的指令,即不执行if后的指令。使用分支预测技术,当处理已经排序的数组时,在若干次data[c]>=128都不成立时(或第一次不成立时,取决于分支预测的实现),CPU预测这个分支是始终会跳转到循环开始的指令时,这个时候CPU将保持有效的执行,不需要重新等待到新的地址取指;同样,当data[c]>=128条件成立若干次后,CPU也可以预测这个分支是不必跳转的,那么这个时候CPU也可以保持高效执行。
相反,如果是无序的数组,CPU的分支预测在很大程度上都无法预测成功,基本就是50%的预测成功概率,这将消耗大量的时间,因为CPU很多时间都会等待取指单元重新取指。