Skip to content

Commit 48347af

Browse files
committed
Create Why-is-processing-a-sorted-array-faster-than-an-unsorted-array.md
1 parent 6cd09c9 commit 48347af

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
## 为什么处理排序的数组要比非排序的快
2+
### 问题
3+
以下是**c++**的一段非常神奇的代码。由于一些奇怪原因,对数据排序后奇迹般的让这段代码快了近6倍!!
4+
```
5+
#include <algorithm>
6+
#include <ctime>
7+
#include <iostream>
8+
9+
int main()
10+
{
11+
// Generate data
12+
const unsigned arraySize = 32768;
13+
int data[arraySize];
14+
15+
for (unsigned c = 0; c < arraySize; ++c)
16+
data[c] = std::rand() % 256;
17+
18+
// !!! With this, the next loop runs faster
19+
std::sort(data, data + arraySize);
20+
21+
// Test
22+
clock_t start = clock();
23+
long long sum = 0;
24+
25+
for (unsigned i = 0; i < 100000; ++i)
26+
{
27+
// Primary loop
28+
for (unsigned c = 0; c < arraySize; ++c)
29+
{
30+
if (data[c] >= 128)
31+
sum += data[c];
32+
}
33+
}
34+
35+
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
36+
37+
std::cout << elapsedTime << std::endl;
38+
std::cout << "sum = " << sum << std::endl;
39+
}
40+
```
41+
42+
43+
- 没有`std::sort(data, data + arraySize);`,这段代码运行了11.54秒.
44+
- 有这个排序的代码,则运行了1.93秒.
45+
我原以为这也许只是语言或者编译器的不一样的问题,所以我又用Java试了一下。
46+
47+
以下是Java代码段
48+
```
49+
import java.util.Arrays;
50+
import java.util.Random;
51+
52+
public class Main
53+
{
54+
public static void main(String[] args)
55+
{
56+
// Generate data
57+
int arraySize = 32768;
58+
int data[] = new int[arraySize];
59+
60+
Random rnd = new Random(0);
61+
for (int c = 0; c < arraySize; ++c)
62+
data[c] = rnd.nextInt() % 256;
63+
64+
// !!! With this, the next loop runs faster
65+
Arrays.sort(data);
66+
67+
// Test
68+
long start = System.nanoTime();
69+
long sum = 0;
70+
71+
for (int i = 0; i < 100000; ++i)
72+
{
73+
// Primary loop
74+
for (int c = 0; c < arraySize; ++c)
75+
{
76+
if (data[c] >= 128)
77+
sum += data[c];
78+
}
79+
}
80+
81+
System.out.println((System.nanoTime() - start) / 1000000000.0);
82+
System.out.println("sum = " + sum);
83+
}
84+
}
85+
```
86+
87+
结果相似,没有很大的差别。
88+
89+
----------------------------------------------------------
90+
我首先得想法是排序把数据放到了cache中,但是我下一个想法是我之前的想法是多么傻啊,因为这个数组刚刚被构造。
91+
- 到底这是为什么呢?
92+
- 为什么排序的数组会快于没有排序的数组?
93+
- 这段代码是为了求一些无关联的数据的和,排不排序应该没有关系啊。
94+
95+
### 回答
96+
#### 什么是分支预测?
97+
看看这个铁路分岔口
98+
为了理解这个问题,想象一下,如果我们回到19世纪.
99+
你是在分岔口的操作员。当你听到列车来了,你没办法知道这两条路哪一条是正确的。然后呢,你让列车停下来,问列车员哪条路是对的,然后你才转换铁路。
100+
**火车很重有很大的惯性。所以他们得花费很长的时间开车和减速。**
101+
是不是有个更好的办法呢?你猜测哪个是火车正确的行驶方向
102+
- 如果你猜对了,火车继续前行
103+
- 如果你猜错了,火车得停下来,返回去,然后你在换条路。
104+
105+
**如果你每次都猜对了,那么火车永远不会停下来。**
106+
**如果你猜错太多次,那么火车会花费很多时间来停车,返回,然后在启动**
107+
108+
----------------------------------------------------------
109+
**考虑一个if条件语句**:在处理器层面上,这是一个分支指令:
110+
http://i.stack.imgur.com/pyfwC.png
111+
你是个处理器当你看到一个分支的时候。你没办法知道下一条指令在哪的时候,你该怎么办呢?你暂停执行一直等到前面的指令完成,然后你在继续执行正确的下一条指令。
112+
现代的处理器很复杂并且有很长的管道。所以他需要很长的时间"热身"和"冷却"
113+
114+
是不是有个更好的办法呢?你猜测下一个指令在哪!
115+
- 如果你猜对了,你继续执行。
116+
- 如果你猜错了,你需要清理管道,返回到那个出错的分支,然后你才能继续。
117+
118+
**如果你每次都猜对了**,那么你永远不会停
119+
**如果你猜错了太多次**,你就要花很多时间来滚回,重启。
120+
121+
-------------------------------------------------------
122+
这就是分支预测。我承认这不是一个好的类比,因为火车可以用旗帜来作为方向的标识。但是在电脑中,处理器不会知道哪一个分支直到走到最后的时候。
123+
所以怎样能很好的预测,在尽可能的使火车必须返回的次数变小?你看看火车之前的选择历史,如果这个火车往左的概率是99%。那么你猜左,反之亦然。如果每三次走这条路,你选择也按这个规律。
124+
125+
**换句话说,你试着定下一个模式,然后按照这个模式去执行**。这就差不多是分支预测是怎么工作的。
126+
127+
大多数的应用都有很好的分支预测。所以现代的分支预测者通常能实现大于90%的命中率。但是当面对没有模式识别的无法预测的分支,那分支预测基本就没用了。
128+
129+
如果你想知道更多:[Branch predictor" article on Wikipedia](https://en.wikipedia.org/wiki/Branch_predictor).
130+
131+
-----------------------------------------------------
132+
133+
#### 有了前面的说明,问题的来源就是这个if条件判断语句
134+
```
135+
if (data[c] >= 128)
136+
sum += data[c];
137+
```
138+
注意到数据是平局分布在0到255之间的。当数据排好序后,大概有一半的的迭代数据不会进入这个条件语句,在这之后的数据会进入该条件语句.
139+
140+
连续的进入同一片区域很多次,这对分支预测是非常友好的。甚至一个简单的饱和计数器会成功预测出分支除了一些需要转换的数据。
141+
142+
##### 快速理解一下
143+
```
144+
T = branch taken
145+
N = branch not taken
146+
147+
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
148+
branch = N N N N N ... N N T T T ... T T T ...
149+
150+
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
151+
```
152+
但是当数据是完全随机的,分支预测就没什么用了。因为他无法预测随机的数据。因此就会有大概50%的概率预测出错。
153+
```
154+
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
155+
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
156+
157+
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
158+
```
159+
----------------------------------------------------------
160+
161+
##### 我们能做些什么呢
162+
如果编译器无法优化带条件的分支,如果你愿意牺牲代码的可读性换来更好的性能的话,你可以用下面的一些技巧。
163+
164+
165+
```
166+
if (data[c] >= 128)
167+
sum += data[c];
168+
```
169+
替换成
170+
```
171+
int t = (data[c] - 128) >> 31;
172+
sum += ~t & data[c];
173+
```
174+
这消灭了分支,把它替换成按位操作.
175+
176+
(说明:这个技巧不是非常严格的等同于原来的if条件语句。但是在`data[]`所有输入值这个情况下是用的)
177+
178+
**使用的设备参数是:Core i7 920 @ 3.5 GHz**
179+
C++ - Visual Studio 2010 - x64 Release
180+
```
181+
// Branch - Random
182+
seconds = 11.777
183+
184+
// Branch - Sorted
185+
seconds = 2.352
186+
187+
// Branchless - Random
188+
seconds = 2.564
189+
190+
// Branchless - Sorted
191+
seconds = 2.587
192+
```
193+
Java - Netbeans 7.1.1 JDK 7 - x64
194+
```
195+
// Branch - Random
196+
seconds = 10.93293813
197+
198+
// Branch - Sorted
199+
seconds = 5.643797077
200+
201+
// Branchless - Random
202+
seconds = 3.113581453
203+
204+
// Branchless - Sorted
205+
seconds = 3.186068823
206+
```
207+
意见:
208+
- 用了分支:没有排序和排序的数据有很大的区别
209+
- 用了上面的技巧:对于排没排序的数据,没有很大的区别
210+
- 在使用C++的情况下,用了小技巧事实上比用排好序的分支要慢上一点点。
211+
212+
一般的建议是尽量避免在关键循环上出现对数据很依赖的分支。(就像这个例子)
213+
214+
------------------------------------------------
215+
216+
更新:
217+
218+
- GCC 4.6.1 用了 `-O3` or `-ftree-vectorize`在64位机器上,数据有没有排序,都是一样快。
219+
**...**
220+
**...**
221+
**...**
222+
等等一下
223+
说明了现代编译器发展的已足够成熟能够各种疯狂的优化代码
224+
225+
### 相关内容
226+
227+
CPU的流水线指令执行
228+
229+
想象现在有一堆指令等待CPU去执行,那么CPU是如何执行的呢?具体的细节可以找一本计算机组成原理来看。CPU执行一堆指令时,并不是单纯地一条一条取出来执行,而是按照一种流水线的方式,在CPU真正指令前,这条指令就像工厂里流水线生产的产品一样,已经被经过一些处理。简单来说,一条指令可能经过过程:取指(Fetch)、解码(Decode)、执行(Execute)、放回(Write-back)。
230+
231+
假设现在有指令序列ABCDEFG。当CPU正在执行(execute)指令A时,CPU的其他处理单元(CPU是由若干部件构成的)其实已经预先处理到了指令A后面的指令,例如B可能已经被解码,C已经被取指。这就是流水线执行,这可以保证CPU高效地执行指令。
232+
233+
分支预测
234+
235+
如上所说,CPU在执行一堆顺序执行的指令时,因为对于执行指令的部件来说,其基本不需要等待,因为诸如取指、解码这些过程早就被做了。但是,当CPU面临非顺序执行的指令序列时,例如之前提到的跳转指令,情况会怎样呢?
236+
237+
取指、解码这些CPU单元并不知道程序流程会跳转,只有当CPU执行到跳转指令本身时,才知道该不该跳转。所以,取指解码这些单元就会继续取跳转指令之后的指令。当CPU执行到跳转指令时,如果真的发生了跳转,那么之前的预处理(取指、解码)就白做了。这个时候,CPU得从跳转目标处临时取指、解码,然后才开始执行,这意味着:CPU停了若干个时钟周期!
238+
239+
这其实是个问题,如果CPU的设计放任这个问题,那么其速度就很难提升起来。为此,人们发明了一种技术,称为branch prediction,也就是分支预测。分支预测的作用,就是预测某个跳转指令是否会跳转。而CPU就根据自己的预测到目标地址取指令。这样,即可从一定程度提高运行速度。当然,分支预测在实现上有很多方法。
240+
241+
242+
**stackoverflow链接**
243+
http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array

0 commit comments

Comments
 (0)