为什么处理有序数组比无序数组快?

有兴趣学习教流c/c++的小伙伴可以加群:941636044

问题

由于某些怪异的原因,下面这段C++代码表现的异乎寻常—-当这段代码作用于有序数据时其速度可以提高将近6倍,这真是令人惊奇。

#include <algorithm>

#include <ctime>

#include <iostream>

int _tmain (int argc , _TCHAR * argv [])

{

//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 eclapsedTime = static_cast<double >(clock() - start) / CLOCKS_PER_SEC;

std::cout << eclapsedTime << std::endl;

std::cout << "sum = " << sum << std::endl;

return 0;

}

如果把 std::sort(data, data+arraySize) 去掉,这段代码耗时11.54秒。

对于有序数据,这段代码耗时1.93秒

起初我以为这可能是某一种语言或某一个编译器发生的异常的事件,后来我在java语言写了个例子,如下:

import java.util.Arrays;

import java.util.Random;

public class Test_Sorted_UnSorted_Array {

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

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

if(data[c] >=128)

sum += data[c];

}

}

System. out.println((System. nanoTime() - start) / 1000000000.0);

System. out.println( "sum = " + sum);

}

}

上述例子运行结果和前面C++例子运行的结果差异,虽然没有C++中那么大,但是也有几分相似。

对于上面的问题,我首先想的原因是排序可能会导致数据有缓存,但是转念一想之前原因有点不切实际,因为上面的数组都是刚刚生成的,所以我的问题是:

上述代码运行时到底发生了什么?

为什么运行排好序的数组会比乱序数组快?

上述代码求和都是独立的,而顺序不应该会产生影响。

回答

其实,你是分支预测(branch prediction )失败的受害者。

什么是分支预测?

考虑一个铁路枢纽:

Imageby Mecanismo, via Wikimedia Commons. Used und

为了便于讨论,假设现在是1800年,这时候还没有出现远程或广播通讯工具。

你是一个铁路枢纽的工人。当你听到火车开来时,你不知道这个火车要走哪一条路,只有让火车停下来询问列车长火车要开往哪,最后你将轨道切换到相应的方向。

火车的质量非常大,固惯性很大,因此火车需要经常性的加速减速。

有没有更好的方法喃?可以猜火车将行驶的方向应该是可行的!

如果猜对了,火车继续往前走;

如果猜错了,列车长会让火车停下来,并后退,然后告诉你正确的方向,然后火车重新启动开往正确的方向。

考虑一个if语句:在处理器级别上,他是一个分支指令:

有兴趣交流学习c/c++的小伙伴可以加群:941636044

你来扮演处理器,当你遇到一个分支,你不知道它要走哪条路,该怎么办?你可以停止执行并等待直到之前的指令执行完。然后继续执行正确路径的指令。

有没有更好的方法喃?可以猜测哪个分支将要被执行!

如果猜对了,继续执行;

如果猜错了,你需要刷新管道并且回退到该分支,重新启动执行正确的方向。

如果每次都能猜对,整个执行过程就不会停止。

如果经常猜错,就需要在停止、回退、重新执行上花费非常多的时间。

这就是分支预测。不得不承认这不是一个最好的比喻因为火车可以仅仅使用一个标志表示其前进的方向。但是对于计算机,直到最后时刻,处理器是不知道哪条分支被执行。

想想可以使用什么预测策略使得火车回退的次数最少?哈哈,可以利用历史数据!如果火车100次有99次都是向左,那么下次预测结果仍向左。如果过去数据是交替的,那么预测结果也是交替的。如果它每3次都换一个方向,那么预测也采用相同的方法。

简而言之,你需要尝试寻找出一个规则(模式)然后按照它进行预测就可以了。分支预测基本上就是这样工作的。

大部分应用程序的分支是很规律的。这也是为什么现代的分支预测的准确率基本上都在90%以上。但是当没有规律、不可预测的分支时候,分支预测就显得比较拙鸡了。

关于分支预测更多详细的内容可参阅:维基百科

从上面可以得到启发,这个问题的“罪魁祸首”就是 if 语句

if (data[c] >= 128)

um += data[c];

注意到数据是在0到255均匀分布的。当排好序后,小于等于128的前半部分是不会执行if语句的,大于128的后半部分都会进入if语句。

这是非常有好的分支预测因为分支会连续多次执行相同的分支。即使是一个简单的饱和计数器也会预测正确除去当变换方向后的少数几个。

快速可视化

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)

那这种情况下该怎么做呢?

如果编译器不能将分支优化为有条件的移动,这时候可以尝试一些 Hacks ,如果能够可以牺牲可读性的表现。

将下面代码

if (data[c] >= 128)

sum += data[c];

替换为:

int t = (data[c] - 128) >> 31;

sum += ~t & data[c];

用一些按位操作取代分支判断,这样就去除了分支。(注意:这个 hacks 并不是和if语句严格相等,但是在我们这个例子里,对输入数组data的所有值都是正确的)

Benchmarks: 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</span></code>

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</span></code>

观察可得:

在分支情况下:排序数组和乱序数组之间的结果有着巨大的差异。

在 Hack 方式下:对于排序和乱序的结果则没有差异。

在C++中,对于排序数组,Hack 会比分支有一点点慢。

一般的经验法则是避免数据依赖分支在一些特殊的循环中。

64位机器下,GCC 4.6.1附带选项-O3或者-ftree-vectorize可以产生一个条件移动。因此对于有序和乱序数据都是一样快。

VC++2010不能够产生条件移动对于这样的分支。

英特尔编译器11同样可以做一些神奇的事。它通过互换两个循环,从而提升了不可预测的分支外循环。因此,它不但能够避免误预测,而且速度上可以达到VC++和GCC的两个快。换句话说,ICC利用了测试回路打破了benchmark。

如果用英特尔编译器执行没有分支的代码,它仅仅出右向量化(out-right vectorizes it),并且和带分支同样快。

通过上面说明,即使比较成熟的现代编译器在优化代码的上可以有很大的不同。

最后,有兴趣一起学习交流c/c++的小伙伴可以加群:941636044

原文地址:https://www.cnblogs.com/2f3d/p/10181954.html

时间: 2024-10-14 12:11:14

为什么处理有序数组比无序数组快?的相关文章

java面向对象的有序数组和无序数组的比较

package aa; class Array{ //定义一个有序数组 private long[] a; //定义数组长度 private int nElems; //构造函数初始化 public Array(int max){ a = new long[max]; nElems = 0; } //size函数 public int size(){ return nElems; } //定义添加函数 public void insert(long value){ //将value赋值给数组成员

为什么有序数组比无序数组快呢?

来自stackoverflow的题目Why is processing a sorted array faster than an unsorted array? Here is a piece of C++ code that seems very peculiar. For some strange reason, sorting the data miraculously makes the code almost six times faster: #include <algorithm

数组结构之数组

数据结构之数组的运用,无非是增删查操作,就有序数组和无序数组进行这三种操作: 一.查找 (1)无序数组查找特定元素,线性查找: 1 public static void unSortSearchKey(int arr[], int key) { 2 for (int i = 0; i < arr.length; i++) { 3 if(arr[i]==key){ 4 System.out.println(key+"在无序数组中索引为的"+i+"个位置"); 5

有序和无序数组的二分搜索算法

题目意思 1.给定有序数组A和关键字key,判断A中是否存在key,如果存在则返回下标值,不存在则返回-1. 2.给定无序数组A和关键字key,判断A中是否存在key,如果存在则返回1,不存在则返回0. 对于1.2问题,我们都可以简单的写出O(n)的从头到尾为的扫描算法,这里就不在累赘,这里我们讨论的是基于二分查找的算法,使其时间在渐进意义上达到O(logn). 对于有序的数组,很"容易"写出基于二分的函数. 那么问题2,对于无序数组,怎么查找呢?这里我们用到了快速排序的划分原则.算法

快速查找无序数组中的第K大数?

1.题目分析: 查找无序数组中的第K大数,直观感觉便是先排好序再找到下标为K-1的元素,时间复杂度O(NlgN).在此,我们想探索是否存在时间复杂度 < O(NlgN),而且近似等于O(N)的高效算法. 还记得我们快速排序的思想麽?通过“partition”递归划分前后部分.在本问题求解策略中,基于快排的划分函数可以利用“夹击法”,不断从原来的区间[0,n-1]向中间搜索第k大的数,大概搜索方向见下图: 2.参考代码: 1 #include <cstdio> 2 3 #define sw

无序数组中第Kth大的数

题目:找出无序数组中第Kth大的数,如{63,45,33,21},第2大的数45. 输入: 第一行输入无序数组,第二行输入K值. 该是内推滴滴打车时(2017.8.26)的第二题,也是<剑指offer>上最小k个数的变形.当时一看到题,这个不是用快排吗?然后就写了,结果始终没有通过,遗憾的超时提交了.错误点主要在于,这里求的是第K大的数,而若是我们使用K去判断快排得到的下标,得到的是第K个数(等同于排序以后从左往右下标为K-1),而题中隐藏的意思等同于排序以后从 右往左数第K个数.所写在写代码

求无序数组的中位数

中位数即是排过序后的处于数组最中间的元素. 不考虑数组长度为偶数的情况.设集合元素个数为n. 简单的想了下:思路1) 把无序数组排好序,取出中间的元素            时间复杂度 采用普通的比较排序法 O(N*logN)            如果采用非比较的计数排序等方法, 时间复杂度 O(N), 空间复杂度也是O(N). 思路2)           2.1)将前(n+1)/2个元素调整为一个小顶堆,          2.2)对后续的每一个元素,和堆顶比较,如果小于等于堆顶,丢弃之,

有1,2,3一直到n的无序数组,排序

题目:有1,2,3,..n 的无序整数数组,求排序算法.要求时间复杂度 O(n), 空间复杂度O(1). 分析:对于一般数组的排序显然 O(n) 是无法完成的. 既然题目这样要求,肯定原先的数组有一定的规律,让人们去寻找一种机会. 例如:原始数组: a = [ 10, 6,9, 5,2, 8,4,7,1,3 ] ,如果把它们从小到大排序且应该是 b = [1,2,3,4,5,6,7,8,9,10 ],也就是说: 如果我们观察 a --> b 的对映关系是: a[i] 在 b 中的位置是 a[i]

二分法查找(数组元素无序)

问题描述: 一数组,含有一堆无序数据,首先将数据按顺序排列,再用二分法实现某个元素的查找,若找到,返回该元素在数组中的下表,否则,返回不存在提示信息. #include<stdio.h> #include<stdlib.h> int *bubble_sort(int a[],int n)//冒泡排序(将数据升序排列) { int i; int j; int tmp; for(j=0;j<n-1;++j)//n个元素需要排序n-1趟 { for(i=0;i<n-j-1;+