RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 626264
Accepted
Grundy
Grundy
Asked:2020-02-10 23:29:30 +0000 UTC2020-02-10 23:29:30 +0000 UTC 2020-02-10 23:29:30 +0000 UTC

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

  • 772

这是一个看起来很奇怪的 C++ 代码示例。出于某种原因,对数据进行排序后,代码的运行速度几乎快了六倍。

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Заполнение данными
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! С этой строкой следующий цикл работает быстрее
    std::sort(data, data + arraySize);

    // Проверка
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // Основной цикл
        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 秒。
  • 使用排序数据 - 1.93 秒。

起初,我以为语言或编译器有问题。所以我尝试使用 Java。

import java.util.Arrays;
import java.util.Random;

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);
    }
}

结果,获得了类似的结果,但差距较小。


我的第一个想法是排序的时候,数据进入缓存,但后来我认为这是愚蠢的,因为刚刚创建了数组。

  • 发生了什么?
  • 为什么排序的数组比未排序的数组处理得更快?

问题翻译为什么处理排序数组比处理未排序数组更快?

java
  • 4 4 个回答
  • 10 Views

4 个回答

  • Voted
  1. Best Answer
    Grundy
    2020-02-05T03:15:49Z2020-02-05T03:15:49Z

    答案翻译:@Mysticial

    您是分支预测器错误的受害者。


    什么是分支预测?

    考虑一个铁路枢纽:


    图片由 Mecanismo 提供,来自维基共享资源。在CC-By-SA 3.0 许可下使用。

    现在想象一下,我们回到了 19 世纪,无线电发明之前。

    您是铁路枢纽站操作员,您听到火车驶来。你不知道他应该走哪条路。你停下火车,问司机他想去哪里。然后将开关设置到所需位置。

    火车很重,惯性很大。因此,启动和停止需要很长时间。

    有没有更好的办法?你可以猜出火车将开往哪里!

    • 如果你猜对了,火车会不停地继续行驶。
    • 如果你犯了一个错误,司机会停下火车,回来,并大喊你改变轨道。然后他可以继续移动。

    如果你每次都猜对了,火车就永远不会停。
    如果你经常犯错误,火车将失去很多时间来停止、返回和加速。


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

    在此处输入图像描述

    你是一个处理器,你看到了分支。您不知道将选择哪个分支。你该怎么办?您停止执行并等待上一条指令完成。然后你沿着正确的路径继续执行。

    现代处理器很复杂,流水线很长。因此,“预热”和“停止”需要很长时间。

    有没有更好的办法?你可以猜到哪个分支将被执行!

    • 如果你猜对了,执行就会继续。
    • 如果出错,必须重置管道并回滚到分支。然后您可以从所需的分支继续。

    如果你每次都猜对了,执行将永远不会停止。
    如果经常出错,停止、回滚和重启会花费很多时间。


    这是过渡预测。我承认,这不是最好的比喻,因为火车可以用旗帜发出信号来指示方向。但在计算机中,处理器直到最后一刻才知道将采取哪个方向。

    那么在猜测的时候选择哪种策略。尽量减少次数。火车应该什么时候返回并改道?你可以看到历史!如果火车 99% 的时间都在向左行驶,那么猜测就是:向左行驶。如果它们交替出现,那么猜测也会交替出现。如果每三次选择一条路径,则可以假设相同...

    换句话说,您正在尝试定义一种行为模式并遵循它。这就是过渡预测器的工作原理。

    大多数应用程序都有明确定义的分支。因此,现代分支预测器的正确猜测百分比通常 > 90%。但是当他们遇到具有未定义模式的不可预测的分支时,分支预测器实际上是无用的。

    进一步阅读:“跳跃预测器”维基百科文章。


    如上所述,问题在于这个 if 语句:

    if (data[c] >= 128)
        sum += data[c];
    

    请注意,数据在 0 到 255 之间均匀分布。
    当数据排序时,前半部分不会进入 if 语句。然后,在 if 语句中,所有其余部分都将消失。

    这对预测器来说非常好,因为连续多次选择相同的分支。
    即使是具有饱和度的简单计数器也能正确预测方向,除非在方向改变之后。

    快速可视化:

    T = ветка выбрана
    N = ветка не выбрана
    
    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  (легко прогнозировать)
    

    但是当数据是随机的时,预测器就没有用了,因为它不能预测随机数据。
    因此,错误预测的概率可能约为 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 ...   (полностью случайно - тяжело прогнозировать)
    

    那么可以做什么呢?

    如果编译器不能优化分支选择,如果您愿意为了性能牺牲可读性,您可以尝试一些 hack。

    代替:

    if (data[c] >= 128)
        sum += data[c];
    

    在:

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

    这消除了分支并用一些按位运算代替它。

    (请注意,此 hack 并不完全等同于原始条件。但在这种情况下,它会为来自 的所有输入值生成正确的结果data[]。)

    基准测试:酷睿 i7 920 @ 3.5 GHz

    C++ - Visual Studio 2010 - x64 版本

    //  Ветвление - случайно
    seconds = 11.777
    
    //  Ветвление - сортировано
    seconds = 2.352
    
    //  Без ветвления - случайно
    seconds = 2.564
    
    //  Без ветвления - сортировано
    seconds = 2.587
    

    Java-Netbeans 7.1.1 JDK 7-x64

    //  Ветвление - случайно
    seconds = 10.93293813
    
    //  Ветвление - сортировано
    seconds = 5.643797077
    
    //  Без ветвления - случайно
    seconds = 3.113581453
    
    //  Без ветвления - сортировано
    seconds = 3.186068823
    

    观察:

    • 分支:排序数据和未排序数据之间的巨大差异。
    • 有一个黑客:排序和未排序的数据之间没有区别。
    • 在 C++ 的情况下,使用 hack 时,结果比对排序数据使用分支时稍微慢一些。

    一般规则是避免在关键循环中使用数据相关条件。(如本例所示)


    更新 :

    • -O3带有或在 x64 上的GCC 4.6.1-ftree-vectorize可以生成 CMOV 指令。在这种情况下,已排序或未排序的数据之间没有区别 - 两种情况都很快。

    • VC++ 2010 不允许生成分支 CMOV,即使在使用/Ox.

    • 英特尔编译器 11 做了一些惊人的事情。它交换两个循环,从而将不可预测的分支推入外循环。所以它不仅不受预测错误的影响,而且生成速度是 VC++ 和 GCC 的两倍!换句话说,ICC 利用测试周期击败了测试......

    • 如果您在没有分支的情况下为英特尔编译器提供代码,它将对其进行矢量化……而且它与分支(带循环)一样快。

    这表明即使是成熟的编译器在优化代码的能力方面也可能彼此不同......

    • 78
  2. αλεχολυτ
    2020-02-04T19:23:58Z2020-02-04T19:23:58Z

    这种影响很可能是由于处理器分支预测块的存在而发生的。当数据排序后,分支成

    if (data[c] >= 128)
    

    只改变一次(起初条件失败,然后总是失败)并且预测效果更好。在未排序的序列上,更难预测它是否有效if。如果将if其完全删除,您可以看到存在或不存在sort不会影响计算的持续时间。

    PS TC 代码似乎是从enSO问题中借来的。因此,要获得更详细的描述,您可以点击链接。正如我已经在评论中指出的那样,我的回答不是翻译。

    • 14
  3. MobiDevices
    2020-02-08T18:18:00Z2020-02-08T18:18:00Z

    您正在观察工作中的过渡预测器。这是一个属于处理器一部分的设备,具有流水线架构,可预测是否会在可执行程序中采用条件分支。

    对于排序数组,数据data[c] >= 128对于一个值带改变一次并且对于所有后续值都变为真。它们很容易被处理器预测(现代处理器中的分支预测准确率超过 90%)。使用未排序的数组,您需要为程序的分支速度付出代价。

    如果没有分支预测,流水线必须等到条件分支指令被执行才能进行下一次提取。分支预测器避免浪费时间试图找出分支。

    • 7
  4. Victor Khovanskiy
    2020-02-04T19:23:44Z2020-02-04T19:23:44Z

    最有可能的是,在排序时,数组data进入了处理器缓存。如果我们考虑到数组的小尺寸,那么它必须完全落入 1 个缓存行。

    • 处理器缓存
    • 1

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5