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

27157
在这段C++代码中,对数据进行排序(在定时区域之前)可以使主循环的速度提高约6倍。
#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 << '\n';
    std::cout << "sum = " << sum << '\n';
}
  • 没有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)
    {
        // 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);
    }
}

与类似但较不极端的结果。
我的第一个想法是排序会将数据放入缓存中,但这是愚蠢的,因为数组刚刚生成。
  • 到底发生了什么?
  • 为什么处理排序后的数组比处理未排序的数组快?

代码正在对一些独立的项求和,所以顺序不应该有影响。


相关/后续问答关于使用不同/较新编译器和选项产生相同效果的问题:


119
另一个观察结果是,您不需要对数组进行排序,只需要使用值128对其进行划分即可。排序的时间复杂度为n*log(n),而划分的时间复杂度仅为线性。基本上只需要运行快速排序划分步骤一次,选择值为128作为枢轴。不幸的是,在C ++中只有nth_element函数,它按位置进行划分,而不是按值进行划分。 - Šimon Hrabec
46
这是一个实验,可以证明分区已经足够:创建一个无序但已分区的数组,并填充随机内容。测量时间。对其进行排序。再次测量时间。这两个测量结果应该基本相同。(实验2:创建一个随机数组。测量时间。对其进行分区。再次测量时间。您应该会看到与排序相同的加速效果。您可以将这两个实验合并为一个。) - Jonas Kölker
41
顺便说一下,在苹果M1上,代码在未排序的情况下运行需要17秒,排序后只需要7秒,因此在RISC架构上,分支预测惩罚并不那么严重。 - Piotr Czapla
36
这取决于编译器。如果编译器为这个特定的测试生成无分支汇编代码(例如作为使用 SIMD 向量化的一部分,就像在为什么处理未排序的数组与处理已排序的数组在现代 x86-64 clang 中速度相同?中所述的那样,或者只是使用标量 cmovgcc 优化标志 -O3 使代码比 -O2 更慢)),那么有序或无序并不重要。但是当问题不像计数那样简单时,不可预测的分支仍然是一个非常真实的问题,因此删除这个问题是不明智的。 - Peter Cordes
20
公正地说,尽管如此,将其分区仍然不值得,因为分区需要根据相同的array[i]>128比较进行条件复制或交换。(除非您要多次计数,并且希望将数组的大部分分区以使其仍然快速,在一些附加或修改后未分区的部分中出现错误预测)。如果您可以让编译器执行此操作,最好使用SIMD进行向量化,或者至少在数据不可预测时使用无分支标量。(请参见上面的评论获取链接。) - Peter Cordes
显示剩余5条评论
26个回答

894

官方的回答可以从以下来源获取:

  1. Intel - 避免分支预测错误的代价
  2. Intel - 分支和循环重组以防止预测错误
  3. 科学论文 - 分支预测计算机体系结构
  4. 书籍:J.L. Hennessy, D.A. Patterson: 计算机体系结构:量化方法
  5. 科学出版物中的文章:T.Y. Yeh, Y.N. Patt 在分支预测上做了很多工作。

你也可以从这个可爱的图表中看到为什么分支预测器会变得混淆。

2比特状态图

原始代码中的每个元素都是随机值。

data[c] = std::rand() % 256;

因此,当std::rand()发生变化时,预测器将更改方向。

另一方面,一旦排序完成,预测器将首先进入强烈未采取的状态,当值变为高值时,预测器将在三次运行中从强烈未采取完全转变为强烈采取。



855

在同一行中(我认为没有任何答案强调这一点),值得一提的是有时候(尤其是在性能至关重要的软件中,比如Linux内核),你会发现像以下代码一样的if语句:

if (likely( everything_is_ok ))
{
    /* Do something */
}

或者类似的:

if (unlikely(very_improbable_condition))
{
    /* Do something */    
}

likely()unlikely()实际上都是宏定义,使用类似GCC的__builtin_expect来帮助编译器插入预测代码,以考虑用户提供的信息,从而更倾向于某个条件。GCC支持其他内建函数,可以改变正在运行程序的行为或发出低级指令,例如清除缓存等,请参阅此文档了解可用的GCC内置函数。

通常,这种优化主要出现在硬实时应用程序或嵌入式系统中,其中执行时间很重要且至关重要。例如,如果您正在检查仅发生1/10000000次的某些错误条件,则为什么不通知编译器呢?这样,按默认情况下,分支预测将假定条件为假。


832

C++中常用的布尔运算会在编译程序中产生很多分支。如果这些分支位于循环内部并且难以预测,它们会显著减慢执行速度。布尔变量存储为8位整数,false值为0true值为1

布尔变量是过度确定的,因为所有具有布尔变量作为输入的运算符都会检查输入是否具有除01之外的任何其他值,但具有布尔变量作为输出的运算符只能产生01。这使得带有布尔变量作为输入的运算比必要的低效。

考虑下面的示例:

bool a, b, c, d;
c = a && b;
d = a || b;

这通常由编译器以下列方式实现:

bool a, b, c, d;
if (a != 0) {
    if (b != 0) {
        c = 1;
    }
    else {
        goto CFALSE;
    }
}
else {
    CFALSE:
    c = 0;
}
if (a == 0) {
    if (b == 0) {
        d = 0;
    }
    else {
        goto DTRUE;
    }
}
else {
    DTRUE:
    d = 1;
}

这段代码远非最优。如果出现预测错误,分支可能会花费很长时间。如果确定操作数除了 01 之外没有其他值,布尔运算可以变得更加高效。编译器不做出这种假设的原因是如果变量未初始化或来自未知源,则变量可能具有其他值。如果 ab 已经被初始化为有效值或来自产生布尔输出的运算符,则上述代码可以进行优化。优化后的代码如下:

char a = 0, b = 1, c, d;
c = a & b;
d = a | b;

char被用来代替bool,以便可以使用按位运算符(&|)而不是布尔运算符(&&||)。按位运算符是单个指令,只需一个时钟周期即可完成。即使ab有除01之外的其他值,OR运算符(|)也会起作用。如果操作数具有除01之外的其他值,则AND运算符(&)和异或运算符(^)可能会给出不一致的结果。

~不能用于NOT。相反,您可以通过将变量与1异或来对已知为01的变量执行布尔NOT:

bool a, b;
b = !a;

可进行优化:

char a = 0, b;
b = a ^ 1;

如果b是一个在afalse时不应被计算的表达式,那么不能用a & b替换a && b&&不会计算b,但&会)。同样地,如果b是一个在atrue时不应被计算的表达式,则不能使用a | b替换a || b

如果操作数是变量而不是比较结果,则使用位运算符更加优越:

bool a; double x, y, z;
a = x > y && z < 5.0;

在大多数情况下,它是最优的(除非您预计&&表达式会生成许多分支错误预测)。


494

那是肯定的!...

分支预测会使逻辑运行变慢,因为代码中发生了切换!就像你走直路或弯路一样,肯定是直路走得更快!...

如果数组已排序,则在第一步时条件为假:data[c] >= 128,然后成为整个街道通往终点的真值。这就是如何更快地到达逻辑结尾的方法。另一方面,使用未排序的数组,您需要进行大量的转向和处理,这肯定会使您的代码运行变慢...

看看我为您创建的图像。哪条街道会更快完成?

Branch Prediction

所以在编程中,分支预测会导致进程变慢...

此外,在最后,我们需要知道有两种不同的分支预测方式,它们会对你的代码产生不同的影响:

1. 静态分支预测

2. 动态分支预测

Branch Prediction

请参阅英特尔的这份文档,其中写道:
静态分支预测是微处理器在第一次遇到条件分支时使用的,而动态分支预测用于后续执行条件分支代码。
为了有效地编写代码以利用这些规则,在编写if-else或switch语句时,请首先检查最常见的情况,并逐步向下处理不太常见的情况。对于循环,通常只使用循环迭代器的条件,因此不一定需要对代码进行任何特殊排序以进行静态分支预测。

444

这个问题已经被回答得非常好了,但是我想引起大家对另一篇有趣分析的注意。

最近,这个例子(稍作修改)也被用来演示如何在Windows程序内部对代码进行性能剖析。作者同时展示了如何使用结果来确定代码在排序和未排序情况下花费大部分时间的位置。最后,文章还展示了如何使用HAL(硬件抽象层)的一个鲜为人知的特性,以确定未排序情况下分支预测错误的数量。

链接在这里: 自我剖析演示


7
这篇文章非常有趣(事实上,我已经读完了所有内容),但是它如何回答这个问题呢? - Peter Mortensen
5
@PeterMortensen,您的问题让我有些困惑。例如,这里是那篇文章中一个相关的句子:“当输入未排序时,循环的其余部分需要大量时间。但对于排序的输入,处理器不仅在循环体中花费的时间更少(指偏移量为0x18和0x1C的桶),而且在循环机制上所花费的时间也非常少。”作者试图在已发布的代码的背景下探讨性能分析,并试图解释为什么排序后的情况会更快。 - ForeverLearning

406

正如其他人所提到的,神秘背后的原因是分支预测器

我不是在添加什么,而是用另一种方式解释这个概念。维基百科上有一个简洁的介绍,其中包含文本和图表。我喜欢下面的解释,它使用图表直观地阐述了分支预测器。

在计算机体系结构中,分支预测器是一种数字电路,它试图在确切之前猜测分支(例如if-then-else结构)的走向。分支预测器的目的是改善指令流水线中的流程。分支预测器在许多现代流水线微处理器架构(如x86)中发挥着至关重要的作用。

通常使用条件跳转指令实现双向分支。条件跳转可以是“未被采取”,并继续执行紧随条件跳转后立即遵循的第一个代码分支,或者可以是“已被采取”,并跳转到存储第二个代码分支的程序内存中的不同位置。在条件计算和条件跳转通过指令流水线的执行阶段之前,无法确定条件跳转是否会被采取或不被采取(参见图1)。

figure 1

根据所述情况,我编写了一个动画演示,展示了在不同情况下如何执行流水线中的指令。

  1. 没有分支预测。

没有分支预测,处理器必须等待条件跳转指令通过执行阶段,然后下一条指令才能进入流水线的获取阶段。

示例包含三条指令,第一条是条件跳转指令。后两条指令可以进入流水线,直到条件跳转指令被执行。

without branch predictor

3条指令需要9个时钟周期才能完成。

  1. 使用分支预测器,不进行条件跳转。假设预测结果是进行条件跳转。

enter image description here

3条指令需要7个时钟周期才能完成。

  1. 使用分支预测并进行条件跳转。假设预测不会进行条件跳转。

enter image description here

完成3个指令需要9个时钟周期。

分支预测错误导致的时间浪费等于从取指阶段到执行阶段的流水线级数。现代微处理器往往具有相当长的流水线,因此误判延迟在10到20个时钟周期之间。因此,使流水线更长增加了对更高级分支预测器的需求。

可以看出,似乎我们没有理由不使用分支预测器。

这是一个非常简单的演示,澄清了分支预测器的基本部分。如果那些gif动画让人感到烦恼,请随意将它们从答案中删除,访问者也可以从BranchPredictorDemo获取实时演示源代码。


9
几乎和英特尔的营销动画一样好,他们不仅着迷于分支预测,而且还痴迷于乱序执行,这两个策略都是“推测性”的。在内存和存储器中提前读取(顺序预取到缓冲区)也是推测性的。所有这些因素加在一起。 - mckenzm
8
乱序预测执行使得分支预测变得更有价值,同时隐藏了取指/译码空隙,分支预测和乱序执行可以消除关键路径上的控制相关性延迟。在一个if()代码块里或者其后面的代码可以在分支条件被知道之前就执行。对于像strlenmemchr这样的搜索循环,迭代可以重叠。如果你必须在任何下一次迭代运行之前等待匹配或不匹配结果被知道,那么你将会因为缓存加载和ALU延迟而成为瓶颈,而不是吞吐量。 - Peter Cordes
5
你是否用JavaFX制作了示例应用程序? - Justin Meskan
4
不,它是用Swing制作的。 该代码可在https://github.com/Eugene-Mark/branch-predictor-demo上获得。 - Eugene

311

分支预测收益!

重要的是要理解,分支预测错误并不会减慢程序的运行速度。错失预测的代价就像没有分支预测一样,你需要等待表达式的评估来决定运行哪些代码(下一段将有进一步的解释)。

if (expression)
{
    // Run 1
} else {
    // Run 2
}

无论是if-else还是switch语句,都需要评估表达式以确定应执行哪个块。编译器生成的汇编代码中会插入条件branch指令。
分支指令可以使计算机开始执行不同的指令序列,从而偏离其按顺序执行指令的默认行为(即如果表达式为false,则程序跳过if块的代码),具体取决于某些条件,这在我们的情况下是表达式评估。
话虽如此,编译器试图在实际评估之前预测结果。它将从if块中提取指令,如果表达式结果为true,则非常棒!我们节省了评估所需的时间并在代码中取得了进展;否则,我们正在运行错误的代码,流水线将被清除,并运行正确的块。
可视化:
比方说你需要选择路线1或者路线2。等待你的搭档检查地图,你已经停在 ## 等待,或者你可以选择路线1,如果你很幸运(路线1是正确的路线),那么你就不必等待你的搭档检查地图(你节省了他检查地图所用的时间),否则你只能掉头返回。
尽管刷新流水线非常快,但现在冒这个险是值得的。预测排序数据或慢变化的数据始终比预测快速变化的数据更容易、更好。
 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------

2
刷新流水线速度非常快。但实际上并不是这样的。与从缓存未命中到DRAM相比,它的速度很快,但在现代高性能x86(如英特尔Sandybridge系列)上,它大约需要十几个周期。虽然快速恢复可以避免等待所有旧的独立指令达到退役之前开始恢复,但在错误预测时仍会损失很多前端周期。当Skylake CPU错误预测分支时会发生什么? (每个周期可以完成约4条指令的工作)。对于高吞吐量代码来说是不利的。 - Peter Cordes

271
在ARM上,不需要分支,因为每个指令都有一个4位条件字段,可以测试处理器状态寄存器中可能出现的16种不同的条件之一,并且如果指令的条件为false,则跳过该指令。这消除了对短分支的需求,并且此算法没有分支预测命中。因此,在ARM上,此算法的排序版本将比未排序版本运行得更慢,因为存在额外的排序开销。

此算法的内部循环在ARM汇编语言中看起来会像以下内容:

MOV R0, #0   // R0 = sum = 0
MOV R1, #0   // R1 = c = 0
ADR R2, data // R2 = addr of data array (put this instruction outside outer loop)
.inner_loop  // Inner loop branch label
    LDRB R3, [R2, R1]   // R3 = data[c]
    CMP R3, #128        // compare R3 to 128
    ADDGE R0, R0, R3    // if R3 >= 128, then sum += data[c] -- no branch needed!
    ADD R1, R1, #1      // c++
    CMP R1, #arraySize  // compare c to arraySize
    BLT inner_loop      // Branch to inner_loop if c < arraySize

但这实际上是更大画面的一部分:CMP操作码总是更新处理器状态寄存器(PSR)中的状态位,因为这是它们的目的,但大多数其他指令不会触及PSR,除非您在指令中添加一个可选的S后缀,指定应基于指令结果更新PSR。就像4位条件后缀一样,能够执行不影响PSR的指令是ARM上减少分支需求的机制,也有助于硬件级别的乱序调度,因为在执行更新状态位的操作X后,随后(或并行)可以进行一些明确不应受到状态位影响的其他工作,然后可以测试由X先前设置的状态位的状态。条件测试字段和可选的“设置状态位”字段可以组合使用,例如:
  • ADD R1, R2, R3执行R1 = R2 + R3,不更新任何状态位。
  • ADDGE R1, R2, R3执行相同的操作,仅在先前影响状态位的指令导致大于或等于条件时才执行。
  • ADDS R1, R2, R3执行加法,然后根据结果是否为负、零、进位(对于无符号加法)或溢出(对于有符号加法),更新处理器状态寄存器中的NZCV标志。
  • ADDSGE R1, R2, R3仅在GE测试为真时执行加法,然后根据加法结果更新状态位。

大多数处理器架构没有这种指定是否应为给定操作更新状态位的能力,这可能需要编写额外的代码来保存并稍后恢复状态位,或者可能需要额外的分支,或者可能会限制处理器的乱序执行效率:由于大多数CPU指令集架构在大多数指令之后强制更新状态位的副作用是很难分离哪些指令可以并行运行而不互相干扰。更新状态位具有副作用,因此对代码具有线性化效应。ARM的能力在任何指令上混合和匹配无分支条件测试,并选择在任何指令之后更新或不更新状态位,对于汇编语言程序员和编译器来说都非常强大,并且产生非常高效的代码。

当你不需要分支时,可以避免为短分支刷新流水线所需的时间成本,并且可以避免许多形式的推测评估的设计复杂性。最近发现的许多处理器漏洞(如Spectre等)的缓解措施最初天真的实现表明了现代处理器性能依赖于复杂的推测评估逻辑有多么大的影响。由于具有短流水线和极大减少分支需求,ARM处理器不需要像CISC处理器一样依赖于推测评估。(当然,高端ARM实现确实包括推测评估,但它只是性能故事的一个小部分。)
如果您曾经想知道为什么ARM如此成功,这两种机制的卓越有效性和相互作用(再加上另一种机制,可以在零附加成本下将算术运算符或偏移内存访问运算符的两个参数之一向左或向右进行“桶位移动”)是该架构效率的最大来源之一。 ARM ISA最初的设计师Steve Furber和Roger(现Sophie)Wilson的智慧无法言喻。

4
ARM的另一个创新是添加了S指令后缀,几乎所有指令都可选。如果省略该后缀,则可以防止指令更改状态位(除了CMP指令,它的作用是设置状态位,因此不需要S后缀)。这使得你可以在许多情况下避免使用CMP指令,只要比较的对象是零或类似于零(例如,当R0达到零时,SUBS R0,R0,#1将设置Z(零)位)。条件和S后缀没有任何开销。这是一个相当美妙的ISA。 - Luke Hutchison
4
不添加 S 后缀可以使您在一行中编写多个条件指令,而不必担心其中一个可能会更改状态位,从而可能导致跳过其余的条件指令。 - Luke Hutchison
2
请注意,OP在他们的测量中没有包括排序的时间。即使非排序情况下循环运行得更慢,先进行排序再运行分支x86循环可能会导致总体损失。但是对大数组进行排序需要大量的工作。 - Peter Cordes
3
顺便提一下,在循环中,您可以通过相对于数组末尾进行索引来节省一条指令。在循环之前,设置 R2 = data + arraySize,然后从 R1 = -arraySize 开始。循环底部变为 adds r1, r1, #1 / bnz inner_loop。由于某些原因,编译器不会使用此优化:/。但是,添加的条件执行在本例中与在其他ISA上使用无分支代码(如x86的cmov)并没有根本上的不同。虽然它不够完美:gcc optimization flag -O3 makes code slower than -O2 - Peter Cordes
3
ARM的条件执行确实是对指令进行无操作(NOPs),因此,即使在会出现错误的加载或存储情况下也可以使用它,这与x86的具有内存源操作数的“cmov”不同。大多数指令集架构(ISA),包括AArch64,只具有ALU选择操作。因此,ARM的条件执行可能非常强大,并且在大多数ISA上比无分支代码更高效。 - Peter Cordes
显示剩余2条评论

234

这篇文章涉及到分支预测。什么是分支预测?

  • 分支预测是一种古老的性能优化技术,在现代架构中仍然具有重要意义。虽然简单的预测技术提供了快速查找和功率效率,但它们的错误预测率很高。

  • 另一方面,复杂的分支预测技术(无论是基于神经网络还是两级分支预测的变体)提供更好的预测准确性,但它们消耗更多的功率,而且复杂度呈指数增长。

  • 此外,在复杂的预测技术中,预测分支所需的时间本身就非常长,范围从2到5个周期,这与实际分支的执行时间相当。

  • 分支预测本质上是一个优化(最小化)问题,重点是以最小的资源实现尽可能低的失误率、低功耗和低复杂度。

实际上,有三种不同类型的分支:

前向条件分支 - 基于运行时条件,PC(程序计数器)被修改为指向指令流中前面的一个地址。

后向条件分支 - PC被修改为指向指令流中后面的一个地址。这种分支是基于某些条件的,例如在一个循环末尾的测试表明应该再次执行循环时,就会向后跳转到程序循环的开头。

无条件分支 - 这包括没有特定条件的跳转、过程调用和返回。例如,一个无条件跳转指令可能在汇编语言中被编码为简单的“jmp”,并且指令流必须立即被重定向到跳转指令所指向的目标位置,而一个有条件的跳转可能被编码为“jmpne”,只有在上一个“比较”指令中两个值的比较结果显示这些值不相等时,才会重定向指令流。(x86架构使用的分段寻址方案增加了额外的复杂性,因为跳转可以是“近距离”(在一个段内)或“远距离”(超出段)。每种类型对分支预测算法有不同的影响。)

静态/动态分支预测:当微处理器第一次遇到条件分支时,使用静态分支预测,而在后续执行条件分支代码时则使用动态分支预测。

参考文献:


225

除了分支预测可能会减慢速度外,排序数组还具有另一个优点:

您可以设置停止条件而不仅仅是检查值,这样只需循环相关数据即可忽略其余部分。
分支预测只会错一次。

 // sort backwards (higher values first), may be in some other part of the code
 std::sort(data, data + arraySize, std::greater<int>());

 for (unsigned c = 0; c < arraySize; ++c) {
       if (data[c] < 128) {
              break;
       }
       sum += data[c];               
 }

3
是的,但是排序数组的设置成本为O(N log N),因此如果你排序数组的唯一原因是能够尽早退出,则提前中断并没有帮助。但是,如果您有其他原因来预先排序数组,那么是有价值的。 - Luke Hutchison
1
取决于您对数据进行排序的次数与循环次数的比较。此示例中的排序仅是一个示例,它不必紧接在循环之前。 - Yochai Timmer
3
是的,这正是我在第一条评论中所提出的观点 :-) 你说,“分支预测只会错一次。”但你没有计算排序算法内 O(N log N) 的分支预测错误的数量,这实际上比未排序情况下的 O(N) 分支预测错误更多。因此,你需要使用完整的已排序数据 O(log N) 次才能收回成本(可能实际上更接近于 O(10 log N),这取决于排序算法,例如对于快速排序,由于缓存未命中,需要更多次数 -- 归并排序更具缓存一致性,因此需要更接近 O(2 log N) 次使用才能收回成本)。 - Luke Hutchison
1
一个重要的优化是只做“半个快速排序”,仅对小于目标枢轴值127的项目进行排序(假设在枢轴之前小于或等于枢轴的所有内容都已排序)。一旦到达枢轴,就对枢轴之前的元素求和。这将以O(N)的启动时间运行,而不是O(N log N),尽管仍然会有很多分支预测失误,可能是基于我之前给出的数字的O(5 N)数量级,因为它是半个快速排序。 - Luke Hutchison
来到这里就是为了找到这个确切的答案。提前终止是排序的主要原因。完全限制搜索空间。在我的测试中,通过能够更早地停止循环查找,速度再次提高了2倍。 - Jason Short

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接