这是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 << '\n';
    std::cout << "sum = " << sum << '\n';
}
  • 不无std::sort(data, data + arraySize);代码在11.54秒内运行
  • 根据分类数据 代码在1.93秒内运行

(分类本身需要的时间比这个通过数组的时间要长, 所以如果我们需要计算未知数组, 它实际上不值得做 。)


起初,我以为这只是一种语言或编译器异常, 所以我尝试了爪哇:

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. 英特尔 - 分行和循环重组以防止误判
  3. 科学论文 -- -- 分支预测计算机结构
  4. 书籍:J.L.Hennnesy、D.A. Patterson:计算机结构:定量方法
  5. 发表在科学出版物上的文章:T.Y.Yeh、Y.N.Patt在分支预测中做了许多这些文章。

你也可以从这个可爱的图表图为什么树枝预测器被弄糊涂了

2-bit state diagram

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

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

所以预测器会变形为std::rand()口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交 口交

另一方面,一旦对预测进行分类, 预测器将首先进入一个 强烈未被采纳的状态, 当值变化到高值时, 预测器将分三步走, 从强烈未被采纳到强烈被采纳。


其他回答

分部门预测。

以排序数组数组, 条件data[c] >= 128第一个是false一连串的数值,然后变成true后期所有值。 这很容易预测。 使用一个未排序的阵列, 您支付分支成本 。

我刚读过这个问题及其答案,我觉得缺少答案。

消除我发现在管理下语言中特别出色的分支预测的一个常见方法是, 表格搜索而不是使用分支(虽然我还没有在本案中测试过它 ) 。

如果:

  1. 它是一个小桌子 很可能在处理器中隐藏
  2. 您正在一个非常紧凑的循环中运行着一些东西和/或处理器可以预加载数据。

背景和原因

从处理器的角度来看,您的内存是慢的。为了弥补速度的差异,在您的处理器( L1/L2 缓存) 中嵌入了几个缓存。 想象一下, 您正在做你的好计算, 并发现您需要一个内存。 处理器会得到它的“ 装载” 操作, 并将内存部分装入缓存中, 然后用缓存来进行其余的计算。 因为内存相对缓慢, 此“ 装载” 将会减缓您的程序 。

像分支预测一样,这在Pentium处理器中被优化了:处理器预测,它需要在操作实际到达缓存之前装入一个数据,并试图将数据装入缓存中。我们已经看到,分支预测有时会发生可怕的错误 -- -- 在最坏的情况下,你需要回去等待一个记忆负荷,这将需要永远的时间(我们已看到,分支预测有时会发生可怕的错误)。换句话说,失败的分支预测是坏的,在分支预测失败之后的记忆负荷实在是太可怕了!).

幸运的是,对于我们来说,如果记忆存取模式可以预测,处理器将装在快速缓存中,一切都很好。

我们首先需要知道的是? 虽然小一点一般比较好,但大拇指规则是坚持使用大小为 4096 字节的搜索表格。作为一个上限:如果您查看的表格大于 64K, 可能值得重新考虑 。

构建表格

因此我们发现我们可以创建一个小表格。 接下来要做的是设置一个查找功能。 查找功能通常是使用几个基本整数操作( 以及, 或者, xor, 转换, 转换, 添加, 删除, 或倍增) 的小型函数。 您想要将您的输入通过外观功能转换为表格中某种“ 独一无二的密钥 ” , 这样就可以简单给出您想要它做的所有工作的答案 。

在此情况下 : 128 表示我们可以保留这个值, < 128 表示我们摆脱它。 最简单的方法就是使用“ 和 ” : 如果我们保留它, 我们和它使用 7FFFFFFF; 如果我们想要摆脱它, 我们和它使用 0。 注意 128 也是一种2 的功率, 所以我们可以继续制作一个32768/128 整数的表格, 并填满它 1 0 和很多 7FFFFFFFFFFFF。

受管理语言

毕竟,管理下的语言会用分支来检查阵列的界限,以确保你不会搞砸...

嗯,不确切地说... : -)

在取消管理下语文的这一分支方面,已经做了相当多的工作。

for (int i = 0; i < array.Length; ++i)
{
   // Use array[i]
}

在此情况下, 编译者明显知道边界条件永远不会被击中 。 至少微软 JIT 编译者( 但我预计爪哇会做类似的事情) 将会注意到这一点并完全取消检查 。 WOW 表示没有分支 。 同样, 它也会处理其他明显的例子 。

如果您遇到管理下语言的查询问题 -- -- 关键是添加 a& 0x[something]FFF使边界检查可以预测, 并且看着它更快地发展。

本案的结果

// Generate data
int arraySize = 32768;
int[] data = new int[arraySize];

Random random = new Random(0);
for (int c = 0; c < arraySize; ++c)
{
    data[c] = random.Next(256);
}

/*To keep the spirit of the code intact, I'll make a separate lookup table
(I assume we cannot modify 'data' or the number of loops)*/

int[] lookup = new int[256];

for (int c = 0; c < 256; ++c)
{
    lookup[c] = (c >= 128) ? c : 0;
}

// Test
DateTime startTime = System.DateTime.Now;
long sum = 0;

for (int i = 0; i < 100000; ++i)
{
    // Primary loop
    for (int j = 0; j < arraySize; ++j)
    {
        /* Here you basically want to use simple operations - so no
        random branches, but things like &, |, *, -, +, etc. are fine. */
        sum += lookup[data[j]];
    }
}

DateTime endTime = System.DateTime.Now;
Console.WriteLine(endTime - startTime);
Console.WriteLine("sum = " + sum);
Console.ReadLine();

你是受害者子分支预测失败 。


分会的预测是什么?

考虑铁路交叉点:

Image showing a railroad junction 图像图像图像图像依据创用CC BY-ND 2.CC-By-SA 3.0 CC-By-SA 3.0许可证。

现在,为了争论起见,假设这是在1800年代, 在长途或无线电通信之前。

您是连接点的盲人接线员, 听到火车来电的声音。 您不知道该走哪条路。 您停止了火车, 询问司机他们想要的方向 。 然后您将开关设置得当 。

火车很重,而且有很多惰性, 所以它们需要永远的启动 并放慢速度。

有更好的办法吗?

  • 如果你猜对了,它会继续下去。
  • 如果你猜错了,船长会停下来,后退,喊你开开关。然后它就可以从另一条路重新开始。

如果你每次猜对火车永远不会停下来
如果你猜错太频繁火车会花很多时间停下来 备份 重新开始


考虑如果报表:在加工一级,它是一个分支指令:

Screenshot of compiled code containing an if statement

你是一个处理者,你看见一个分支。你不知道它会走哪条路。你做什么?你停止执行,等待以前的指令完成。然后,你继续走正确的道路。

现代处理器复杂,管道长。 这意味着它们永远需要“暖和”和“慢下来 ” 。

有更好的办法吗?

  • 如果你猜对了,你继续执行。
  • 如果您猜错了, 您需要冲洗管道, 然后滚回分支。 然后您就可以重新启动另一条路径 。

如果你每次猜对死刑将永远不会停止
如果你猜错太频繁,你花了很多时间拖延, 后退,重新开始。


这是分支预测。 我承认这不是最好的比喻, 因为火车只能用旗帜发出方向信号。 但在电脑上, 处理器不知道分支会朝哪个方向前进, 直到最后一刻。

您在战略上如何猜测如何将列车必须返回并沿着另一条路行驶的次数最小化 ? 您看看过去的历史 。 如果列车离开99%的时间, 那么您会猜到离开 。 如果列车转行, 那么您会换个猜想 。 如果列车每走三次, 您也会猜到同样的情况 。

换句话说,你试图找出一个模式 并遵循它。这或多或少是分支预测器的工作方式。

大多数应用程序都有良好的分支。 因此,现代分支预测器通常会达到超过90%的冲击率。 但是,当面对无法预见且没有可识别模式的分支时,分支预测器几乎毫无用处。

进一步读作:维基百科的“Branch 预测器”文章.


正如上面所暗示的,罪魁祸首就是这个说法:

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

请注意数据分布在 0 和 255 之间。 当对数据进行分类时, 大约前半段的迭代不会输入 if 语句 。 在此之后, 它们都会输入 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, ...
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];

这将清除分支, 并替换为一些位元操作 。

(注意这个黑客并不完全等同原始的如果声明。 但在这种情况下,它对于所有输入值都有效。data[].)

基准:核心i7 920@3.5千兆赫

C++ - 2010 - x64 释放

假设情景 时间( 秒)
分处 - 随机数据 11.777
分支 - 分类数据 2.352
无分支 - 随机数据 2.564
无分支 - 排序数据 2.587

Java - Netbeans 7.1.1 JDK 7 - x64

假设情景 时间( 秒)
分处 - 随机数据 10.93293813
分支 - 分类数据 5.643797077
无分支 - 随机数据 3.113581453
无分支 - 排序数据 3.186068823

意见:

  • 与该处:分类和未分类数据之间存在巨大差异。
  • 与哈克人:分类的数据和未分类的数据没有区别。
  • 在 C++ 案中, 黑客的进位实际上比数据排序时的分支慢。

拇指的一般规则是避免在关键循环(如本例)中出现依赖数据的分支。


更新 :

  • GCC 4.6.1 和-O3-ftree-vectorize在 x64 上能够生成一个有条件的移动, 所以分类的数据和未分类的数据之间没有区别, 两者都是快速的 。

    (或稍快:对于已经分类的案件,cmov特别是如果海合会将海合会置于关键道路上,而不是公正add特别是英特尔 之前的英特尔 Broadwellcmov有2个周期的延迟:gcc 优化标记 -O3 使代码慢于 -O2)

  • VC++/2010 即使在/Ox.

  • Intel C+++ 编译器(ICC) 11 做了奇迹般的事情。交换两个循环从而将无法预测的分支拉到外环。 它不仅能避免错误, 而且速度是 VC++ 和 GCC 所能生成的两倍。 换句话说, ICC 利用试流击败基准...

  • 如果您给 Intel 编译者无分支代码, 它会直接向导它... 并且和分支( 循环交换) 一样快 。

这表明即使是成熟的现代编译者 在优化代码的能力上 也会大不相同...

分流收益!

重要的是要理解分支错误控制不会减慢程序。 错误预测的成本就好像不存在分支预测,而你等待着对表达方式的评价来决定运行的代码(下段有进一步的解释 ) 。

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

每当有if-else \ switch语句中,必须评价表达式,以决定应执行哪个区块。在编译器生成的组装代码中,有条件分支分支分支插入说明。

分支指令可导致计算机开始执行不同的指令序列,从而偏离其按顺序执行指令的默认行为(即如果表达式为虚假,程序跳过代码)if(根据某些条件,即我们案件的表达方式评价)

尽管如此,汇编者试图在对结果进行实际评估之前预测结果。if如果表达式是真实的,那么就太好了!我们得到了评估它所需的时间,并在代码中取得了进展;如果不是,我们运行错误的代码,管道被冲洗,正确的模块被运行。

可视化:

假设你需要选择路线1或路线2, 等待你的伴侣检查地图, 你已经停留在 ##,等待, 或者你可以选择路线1, 如果你运气好(路线1是正确的路线), 那么伟大的你不必等待你的伴侣检查地图(你省下时间让他检查地图), 否则你就会转回去。

尽管冲水管道的速度超快,但如今赌博是值得的。 预测分类数据或缓慢变化的数据总是比预测快速变化容易,也好于预测快速变化。

 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------

当对数组进行排序时,数据在 0 到 255 之间分布, 大约在迭代的前半部不会输入if- 声明if报表如下。 )

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

The question is: What makes the above statement not execute in certain cases as in case of sorted data? Here comes the "branch predictor". A branch predictor is a digital circuit that tries to guess which way a branch (e.g. an if-then-else分支预测器的目的是改善教学管道的流量。 分支预测器在实现高效运行方面发挥着关键作用 !

让我们做一些板凳标记 来更好理解它

性能、性能、性能、性能、性能、性能、性能、性能、性能、性能、性能、性能、性能、性能、性能、性if如果条件总是真实的,或者总是假的,处理器中的分支预测逻辑将拾取该模式。另一方面,如果该模式无法预测,那么,if- 声明会更贵得多

让我们用不同的条件来衡量这个循环的性能:

for (int i = 0; i < max; i++)
    if (condition)
        sum++;

以下是环绕时间与不同的真假模式 :

Condition                Pattern             Time (ms)
-------------------------------------------------------
(i & 0×80000000) == 0    T repeated          322

(i & 0xffffffff) == 0    F repeated          276

(i & 1) == 0             TF alternating      760

(i & 3) == 0             TFFFTFFF…           513

(i & 2) == 0             TTFFTTFF…           1675

(i & 4) == 0             TTTTFFFFTTTTFFFF…   1275

(i & 8) == 0             8T 8F 8T 8F …       752

(i & 16) == 0            16T 16F 16T 16F …   490

“A ““真实的假造模式可以使if- 计算速度比“或”慢6倍。良好当然,哪一种模式是好的,哪一种模式是坏的,取决于汇编者的确切指示和具体处理者。

因此,部门预测对业绩的影响是毫无疑问的!