这世上有三样东西是别人抢不走的:一是吃进胃里的食物,二是藏在心中的梦想,三是读进大脑的书java
如下是**c++**的一段很是神奇的代码。因为一些奇怪缘由,对数据排序后奇迹般的让这段代码快了近6倍!!ios
#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);
// Test
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;
}
复制代码
std::sort(data, data + arraySize);
,这段代码运行了11.54秒.如下是Java代码段c++
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
int arraySize = 32768;
int data[] = new int[arraySize];
Random rnd = new Random(0);
for (int c = 0; c < arraySize; ++c)
data[c] = rnd.nextInt() % 256;
// !!! With this, the next loop runs faster
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
}
复制代码
结果类似,没有很大的差异。git
我首先得想法是排序把数据放到了cache中,可是我下一个想法是我以前的想法是多么傻啊,由于这个数组刚刚被构造。github
看看这个铁路分岔口 数组
为了理解这个问题,想象一下,若是咱们回到19世纪.bash
你是在分岔口的操做员。当你听到列车来了,你没办法知道这两条路哪一条是正确的。而后呢,你让列车停下来,问列车员哪条路是对的,而后你才转换铁路方向。less
火车很重有很大的惯性。因此他们得花费很长的时间开车和减速。dom
是否是有个更好的办法呢?你猜想哪一个是火车正确的行驶方向oop
若是你每次都猜对了,那么火车永远不会停下来。 若是你猜错太屡次,那么火车会花费不少时间来停车,返回,而后再启动
考虑一个if条件语句:在处理器层面上,这是一个分支指令:
是否是有个更好的办法呢?你猜想下一个指令在哪!
若是你每次都猜对了,那么你永远不会停 若是你猜错了太屡次,你就要花不少时间来滚回,重启。
这就是分支预测。我认可这不是一个好的类比,由于火车能够用旗帜来做为方向的标识。可是在电脑中,处理器不能知道哪个分支将走到最后。
因此怎样能很好的预测,尽量地使火车必须返回的次数变小?你看看火车以前的选择过程,若是这个火车往左的几率是99%。那么你猜左,反之亦然。若是每3次会有1次走这条路,那么你也按这个三分之一的规律进行。
换句话说,你试着定下一个模式,而后按照这个模式去执行。这就差很少是分支预测是怎么工做的。
大多数的应用都有很好的分支预测。因此现代的分支预测器一般能实现大于90%的命中率。可是当面对没有模式识别、没法预测的分支,那分支预测基本就没用了。
若是你想知道更多:Branch predictor" article on Wikipedia.
if (data[c] >= 128)
sum += data[c];
复制代码
注意到数据是分布在0到255之间的。当数据排好序后,基本上前一半大的的数据不会进入这个条件语句,然后一半的数据,会进入该条件语句.
连续的进入同一个执行分支不少次,这对分支预测是很是友好的。能够更准确地预测,从而带来更高的执行效率。
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
复制代码
可是当数据是彻底随机的,分支预测就没什么用了。由于他没法预测随机的数据。所以就会有大概50%的几率预测出错。
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
复制代码
若是编译器没法优化带条件的分支,若是你愿意牺牲代码的可读性换来更好的性能的话,你能够用下面的一些技巧。
把
if (data[c] >= 128)
sum += data[c];
复制代码
替换成
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
复制代码
这消灭了分支,把它替换成按位操做.
(说明:这个技巧不是很是严格的等同于原来的if条件语句。可是在data[]
当前这些值下是OK的)
使用的设备参数是:Core i7 920 @ 3.5 GHz C++ - Visual Studio 2010 - x64 Release
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
复制代码
Java - Netbeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
复制代码
结论:
通常的建议是尽可能避免在关键循环上出现对数据很依赖的分支。(就像这个例子)
更新:
-O3
or -ftree-vectorize
,在64位机器上,数据有没有排序,都是同样快。 ... ... ...等各类例子说明了现代编译器愈加成熟强大,能够在这方面充分优化代码的执行效率
CPU的流水线指令执行
想象如今有一堆指令等待CPU去执行,那么CPU是如何执行的呢?具体的细节能够找一本计算机组成原理来看。CPU执行一堆指令时,并非单纯地一条一条取出来执行,而是按照一种流水线的方式,在CPU真正指令前,这条指令就像工厂里流水线生产的产品同样,已经被通过一些处理。简单来讲,一条指令可能通过过程:取指(Fetch)、解码(Decode)、执行(Execute)、放回(Write-back)。
假设如今有指令序列ABCDEFG。当CPU正在执行(execute)指令A时,CPU的其余处理单元(CPU是由若干部件构成的)其实已经预先处理到了指令A后面的指令,例如B可能已经被解码,C已经被取指。这就是流水线执行,这能够保证CPU高效地执行指令。
分支预测
如上所说,CPU在执行一堆顺序执行的指令时,由于对于执行指令的部件来讲,其基本不须要等待,由于诸如取指、解码这些过程早就被作了。可是,当CPU面临非顺序执行的指令序列时,例如以前提到的跳转指令,状况会怎样呢?
取指、解码这些CPU单元并不知道程序流程会跳转,只有当CPU执行到跳转指令自己时,才知道该不应跳转。因此,取指解码这些单元就会继续取跳转指令以后的指令。当CPU执行到跳转指令时,若是真的发生了跳转,那么以前的预处理(取指、解码)就白作了。这个时候,CPU得从跳转目标处临时取指、解码,而后才开始执行,这意味着:CPU停了若干个时钟周期!
这实际上是个问题,若是CPU的设计听任这个问题,那么其速度就很难提高起来。为此,人们发明了一种技术,称为branch prediction,也就是分支预测。分支预测的做用,就是预测某个跳转指令是否会跳转。而CPU就根据本身的预测到目标地址取指令。这样,便可从必定程度提升运行速度。固然,分支预测在实现上有不少方法。
stackoverflow连接:
这个问题的全部回答中,最高的回答,获取了上万个vote,还有不少个回答,很是疯狂,你们以为不过瘾能够移步到这里查看
stackoverflow.com/questions/1…
欢迎关注 yunlongn.github.io