为什么处理一个排好序的数组比未排序的数组要快?

文章通过一个Java代码示例展示了排序数组如何提高执行效率,这涉及到计算机的分支预测机制。当数组未排序时,CPU在执行if条件判断时预测分支的准确性降低,影响了执行速度。排序后,由于连续元素更可能有相似性质(如大于或小于某个值),CPU的分支预测成功率提高,从而加快了程序执行。

这是stack overflow上一个非常火热的问题:

为什么对数组排序能提高执行效率?先看下面的代码,执行分为:

  1. 生成随机数放入数组
  2. 对数组进行排序
  3. 对数组中的数字进行累加
public class Main {
    public static void main(String[] args) {
        // 生成随机数
        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;

        // 排序
        Arrays.sort(data);

        // 累加,测试执行时间
        long start = System.nanoTime();
        long sum = 0;
        for (int i = 0; i < 100000; ++i) {
            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);
    }
}
复制代码

排序后的执行时间:2.2350021s 未排序后的执行时间:6.385857s

可以发现:排序后的执行时间是未排序的执行时间的3倍。

为什么会这样,这就跟分支预测有关系,什么是分支预测呢?

计算机在执行某一条指令的过程中,会分为四个步骤:

每个步骤会有对应的电路组件负责。

假设现在有一段程序对应的计算机指令为:

那么计算机应该先执行指令1,然后根据指令1的结果看接下来应该执行哪条指令,假设接下来要执行的就是指令2,那指令流水线会是下图吗?

假设每个步骤要1毫秒,那执行完这两条指令就需要8毫秒,但是前面说过,指令的四个步骤是由特定的组件来执行的,所以其实可以这么执行:

在解码指令1的过程中,就可以读取指令2了,这样执行完两条指令只需要5毫秒,如果后续指令也按这个节奏执行,那么效率提升会很大,比如串行执行3条指令需要3*4=12毫秒,而并行执行3条指令只需要6毫秒,快了一倍了。

那这个跟分支预测有什么关系呢?

我们在来看

假如指令2是一条判断指令,根据判断结果,要么执行指令3,要么执行指令4。

如果不对判断结果进行预测,那么就只能等指令2执行完之后才知道接下来要执行哪条指令,那指令流水线就是下面这样:

执行这三条指令要9毫秒。

但是,如果在执行指令2之前,我们就能预测出指令2的判断结果,假如预测的是指令3,那么指令流水线就可以这样:

相当于在解码指令2的时候,就同时去读取指令3,如果执行完指令2后,发现判断结果和预测的结果是一致的,那么就可以直接把指令3的结果写回了,这样就大大提高效率,执行这3条指令只需要6毫秒。

当然如果执行完指令2后,发现预测错了,那么就不能把指令3的结果写回了,而是得读取并执行正确的指令。

以上就是分支预测,简单总结一下就是,计算机会对分支,比如判断指令的结果进行预测,从而提前去做一些事情,而不是等判断结果出来后才去做事情。

回到上面的Java代码:

for (int c = 0; c < arraySize; ++c) {
    if (data[c] >= 128)
        sum += data[c];
}
复制代码

if语句就相当于是一个判断指令,根据判断结果就会有两个分支:

  1. 要么进行累加
  2. 要么取数组中的下一个数字。

所以,当执行if语句时,计算机就可以进行预测:

  1. 比如预测当前判断的数字是大于128的,那么就可以在判断结果没有出来之前提前进行累加
  2. 或者预测当前判断的数字是小于128的,那么就可以在判断结果没有出来之前提前取下一个数字

只要预测对了,就能提高执行效率,所以,我们要做的就是尽可能让计算机能预测准确,这就是数组排序的作用

比如计算机在执行指令过程中,发现连续10个数字都是大于128的,那么就可以预测接下来的一个数字也是大于128的,所以只要数组是排好序的,那么这个预测就是正确的,从而也就提高了执行效率。

以上,就是分支预测,不知道大家看明白没...

作者:Hoeller
链接:https://juejin.cn/post/7188781090986721339

一个数插入已排好数组并保持原排序规律,通常可按以下步骤实现:先确定插入位置,再将插入位置之后的元素依次后移,最后把该数插入到相应位置。以下是几种不同的实现代码: ### 方法一 ```c #include<stdio.h> #define N 6 int main () { int a[N],t,i,m,n; printf("请输入已经排列好的数组:"); for(i=0;i<=N-2;i++) scanf("%d",&a[i]);//先输入已经排列好的数组 printf("请输入需要插入的数"); scanf("%d",&t);//输入需要插入的数 for(i=0;i<=N-2;i++)//找出要插入的位置 if(t<=a[i]) { n=i+1; printf("新位置=%d\n",n); break; } for(m=N-1;m>=i+1;m--) a[m]=a[m-1];//把大于新数的数往后面移一个 a[n-1]=t; for(i=0;i<=N-1;i++) printf("%4d",a[i]); return 0; } ``` 此方法先找出插入位置,再将插入位置之后的元素后移,最后插入新数,数组大小由宏定义 `N` 确定,预留一个位置给新插入的数 [^1]。 ### 方法二 ```c #include<stdio.h> int main() { int a[10]={1,5,8,23,45,78,88,89,92},t=0; printf("Input a figure:"); scanf("%d",&a[9]); for(int i=9;i>=0;i--){ if(a[i]<a[i-1]){ t=a[i-1]; a[i-1]=a[i]; a[i]=t; } else break; } for(int i=0;i<=9;i++){ printf("%4d",a[i]); } return 0; } ``` 该方法直接将输入的数放在数组末尾,然后从后往前两两比较交换位置,直到满足排序规律 [^2]。 ### 方法三 ```c #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int arr[100], n, i, j, k; //输入数组元素个数 printf("请输入元素个数:"); scanf("%d", &n); //输入已排好数组 printf("请输入已排好数组:\n"); for (i = 0; i < n; i++) { scanf("%d", &arr[i]); } //输入要插入的数 printf("请输入要插入的数:"); scanf("%d", &k); //在已排好数组中找到要插入的位置 for (i = 0; i < n; i++) { if (k < arr[i]) { break; } } //将要插入的数插入到数组中 for (j = n - 1; j >= i; j--) { arr[j + 1] = arr[j]; } arr[i] = k; n++; //输出新数组 printf("新数组为:\n"); for (i = 0; i < n; i++) { printf("%d ", arr[i]); } return 0; } ``` 此方法先输入数组元素个数和已排好数组,再输入要插入的数,找到插入位置后将元素后移并插入新数,数组最大容量为 100 [^3]。 ### 方法四 ```c #include<stdio.h> int main() { int a[11]={1,4,6,9,13,16,19,28,40,100};//定义一个包含a[0]-a[9]共10位数字的一维数组 int number,i=0,j; scanf("%d",&number); if(number>a[9])//如果输入的数字大于a[9] { a[10]=number;//把输入的数字放到a[10] } else { for(i=0;i<10;i++) { if(a[i]>=number) { for(j=9;j>=i;j--)//i的前一位就是插入点 { a[j+1]=a[j];//把i到j的j - i个元素向后移一个 } a[i]=number; break; } } } for(i=0;i<11;i++) { printf("%d\n",a[i]); } return 0; } ``` 该方法先判断输入的数是否大于数组最后一个数,若是则直接放在数组末尾,否则找到插入位置后将元素后移并插入新数 [^4]。 ### 方法五 ```c #include<stdio.h> int main() { int i, j; int num; scanf("%d", &num); int a[10] = { 240, 105, 98, 82, 66, 27, 18, 9, 1 }; for (i = 0; i < 10; i++) { if (a[i] <= a[i + 1]) { if ((num >= a[i]) && (num < a[i + 1])) { for (j = 8; j >= i + 1; j--) a[j + 1] = a[j]; a[i + 1] = num; break; } } else { if ((num <= a[i]) && (num > a[i + 1])) { for (j = 8; j >= i + 1; j--) a[j + 1] = a[j]; a[i + 1] = num; break; } } } for (i = 0; i < 10; i++) printf("%5d", a[i]); return 0; } ``` 此方法考虑了数组和降两种情况,根据不同情况找到插入位置,将元素后移并插入新数 [^5]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值