ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

为什么处理排序的数组比处理未排序的数组更快? | Java Debug 笔记

2021-05-19 21:03:32  阅读:113  来源: 互联网

标签:... Java 数据 arraySize 数组 排序 data 分支


本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看活动链接

提问:为什么处理排序的数组比处理未排序的数组更快?

下面是一个C++代码,显示了一些非常特殊的行为。出于一些奇怪的原因,奇迹般地对数据进行排序会使代码速度快近六倍:

#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)
    {
        for (unsigned c = 0; c < arraySize; ++c)
        {   // Primary loop
            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;
}
复制代码

没有,代码在 11.54 秒内运行。std::sort(data, data + arraySize); 使用排序数据,代码在 1.93 秒内运行。 最初,我认为这可能只是一种语言或编译器异常,所以我尝试了Java:

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)
        {
            for (int c = 0; c < arraySize; ++c)
            {   // Primary loop
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}
复制代码

具有类似但不那么极端的结果。

我首先想到的是,排序将数据带入缓存,但后来我想这是多么愚蠢,因为阵列刚刚生成。

1.这是怎么回事?

2.为什么处理排序的数组比处理未排序的数组更快?

高分回答:

考虑铁路交汇点:

image.png

现在为了争论,假设这是回到19世纪 - 在长途或无线电通信之前。

你是一个路口的操作员,你听到火车来了。你不知道该走哪条路。你停下来问司机他们想要什么方向。然后适当设置开关。

火车很重,有很强的惯性。因此,他们需要永远启动和减速。

还有更好的方法吗?你猜火车往哪个方向开!

如果你猜对了,它将继续。 如果你猜错了,船长会停下来,站起来,对你大喊大叫,让你翻转开关。然后,它可以重新启动到另一条路径。 如果你每次都猜对了,火车就永远也停不下。 如果你猜错了太多,火车将花费大量的时间停止,备份和重新启动。

考虑如果语句:在处理器级别,它是分支指令:

image.png

您是一个处理器,您会看到一个分支。你不知道它会走哪条路。你是做什么工作的?您停止执行并等待之前指令完成。然后,你继续沿着正确的道路。

现代处理器复杂,管道长。因此,他们需要永远"热身"和"慢下来"。

还有更好的方法吗?你猜到树枝会朝哪个方向走!

如果你猜对了,你继续执行。 如果您猜错了,则需要冲洗管道并回滚到分支。然后,您可以沿着其他路径重新启动。 如果你每次都猜对了,执行就永远不能停止。 如果你猜错了太多,你花很多时间拖延,回滚,并重新启动。

这是分支预测。我承认这不是最好的类比, 因为火车可以用旗帜发出方向信号。但在计算机中,处理器直到最后一刻才知道分支将朝哪个方向前进。

那么,您如何从战略上猜测,以尽量减少列车必须备份并沿着另一条道路前进的次数呢?你看看过去的历史!如果火车有99%的时间向左开,那么你猜是左转了。如果它交替,那么你交替你的猜测。如果它每三次走一条路, 你猜的一样...

换句话说,您尝试识别并遵循该模式。 这或多或少是分支预测器的工作原理。

大多数应用程序都有表现良好的分支。因此,现代分支预测器通常会达到>90%的命中率。但是,当面对无法识别的无法识别的分支时,分支预测器几乎毫无用处。

进一步阅读:维基百科上的"分支预测器"文章。

如上所述,罪魁祸首是这种如果声明:

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, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T  ...

       = TTNTTTTNTNNTTT ...   (completely random - impossible to predict)
复制代码

那么,我们能做些什么呢?

如果编译器无法将分支优化为有条件的移动,则如果您愿意牺牲可读性来提高性能,则可以尝试一些******。

取代:

if (data[c] >= 128)
    sum += data[c];
复制代码

跟:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
复制代码

这消除了分支,代之以一些位操作。

(请注意,此***并不严格等同于原始的如果语句。但在这种情况下,它适用于数据的所有输入值[]。

基准: 核心 i7 920 = 3.5 GHz

C++ - 视觉工作室 2010 - x64 发布

场景	时间(秒)
分支 - 随机数据	11.777
分支 - 排序数据	2.352
无分支 - 随机数据	2.564
无分支 - 排序数据	2.587
爪哇 - 网豆 7.1.1 JDK 7 - x64

场景	时间(秒)
分支 - 随机数据	10.93293813
分支 - 排序数据	5.643797077
无分支 - 随机数据	3.113581453
无分支 - 排序数据	3.186068823
复制代码

观察:

与分支:排序数据和未分类数据之间存在巨大差异。 与***:排序数据和未分类数据之间没有区别。 在C++情况下,当数据排序时,***实际上比分支慢一点。 一般的经验法则是避免在关键循环中(例如在本示例中)中依赖数据的分支。

更新:

GCC 4.6.1 带或在 x64 上能够生成有条件移动。因此,排序数据和未分类数据之间没有区别 - 两者都很快。-O3-ftree-vectorize

(或有点快:对于已经排序的情况下,可以慢一点,特别是如果海湾合作委员会把它放在关键路径上,而不仅仅是,特别是在英特尔之前布罗德韦尔有2个周期延迟:gcc优化标志-O3使代码慢于-O2cmovaddcmov)

VC++ 2010 即使在 下也无法为该分支生成有条件的移动。/Ox

英特尔C++编译器(ICC) 11 做了一件奇迹。它交换两个环,从而将不可预知的分支吊到外环。因此,它不仅不受错误预测的影响,而且速度是任何VC++和海合会所能产生的速度的两倍!换句话说,ICC利用测试循环击败了基准。。。

如果你给英特尔编译器无分支代码,它只是右外向量化它。。。和分支(与循环交换)一样快。

这表明,即使是成熟的现代编译器在优化代码的能力上也会大相径庭。。。

标签:...,Java,数据,arraySize,数组,排序,data,分支
来源: https://blog.51cto.com/u_10182395/2787140

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有