这是C++代码的一块 显示一些非常特殊的行为
由于某种原因,对数据进行分类(在时间区之前)奇迹般地使主要循环速度快近六倍:
#include
#include
#include
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(clock()-start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << '\n';
std::cout << "sum = " << sum << '\n';
}
没有 std: sort( 数据, 数据+数组Size); 代码在 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);
}
}
其结果类似,但不太极端。
我的第一个想法是排序 将数据带入缓存, 但这是愚蠢的,因为数组 刚刚生成。
为什么处理一个分类阵列的速度要快于处理一个未分类阵列的速度?
守则正在总结一些独立的术语,因此命令不应重要。
与不同的/后来的汇编者和备选办法具有相同效果:
为什么处理一个未排列的阵列的速度与处理一个用现代 x86-64 叮当的排序阵列的速度相同? gcc 优化标记 -O3 使代码慢于 -O2
我用MATLAB 2011b 和我的MacBook Pro(Intel i7, 64位, 2.4 GHz) 尝试了以下MATLAB 代码的相同代码 :
% Processing time with Sorted data vs unsorted data
%==========================================================================
% Generate data
arraySize = 32768
sum = 0;
% Generate random integer data from range 0 to 255
data = randi(256, arraySize, 1);
%Sort the data
data1= sort(data); % data1= data when no sorting done
%Start a stopwatch timer to measure the execution time
tic;
for i=1:100000
for j=1:arraySize
if data1(j)>=128
sum=sum + data1(j);
end
end
end
toc;
ExeTimeWithSorting = toc - tic;
上述MATLAB代码的结果如下:
a: Elapsed time (without sorting) = 3479.880861 seconds.
b: Elapsed time (with sorting ) = 2377.873098 seconds.
校对:Soup
a: Elapsed time (without sorting) = 19.8761 sec.
b: Elapsed time (with sorting ) = 7.37778 sec.
基于这一点,看来MATLAB比C执行慢了175倍,没有分类,比C执行慢了350倍,换言之,(分支预测)MATLAB执行效果为1.46x,C执行效果为2.7x。
正如其他人已经提到的那样,神秘背后的是部门预测员。
我不是要补充一些东西,而是要用另一种方式解释这个概念。维基文字有一个简明的介绍,里面有文字和图表。我确实喜欢下面的解释,下面用一个图表来用直觉来描述处的预言。
在计算机结构中,分支预测器是一种数字电路,它试图猜测分支(如如果是当时的else结构)将以何种方式进行,然后才能确定这一点。分支预测器的目的是改善教学管道的流量。分支预测器在很多现代管道式微处理器结构(如x86)中实现高有效性能方面发挥着关键作用。双向分支通常是通过有条件的跳跃指令来实施。有条件跳跃可以是“不采取”的,也可以是有条件跳跃后立即实施的代码的第一个分支,或者可以是“获取”的,然后跳到存储第二分支的程序内存中的不同位置。在计算条件和有条件跳动通过指令管道的执行阶段之前,无法确定是否进行有条件跳动(见图1)。
调
根据所述情况,我写了动画演示,以显示在不同情况下如何在管道中执行指示。
没有部门预言家。
没有分支预测,处理器必须等到有条件跳跃指令通过执行阶段后,下一个指令才能进入管道的接货阶段。
该示例包含三个指令, 第一个是有条件跳跃指令。 后两个指令可以进入管道, 直到有条件跳跃指令执行为止 。
调
完成3项指示需要9小时周期。
使用预测器,不要采取有条件的跳跃。让我们假设预测不会采取有条件的跳跃。
调
完成3项指示需要7小时周期。
我们假设预测不会采取有条件的跳跃
调
完成3项指示需要9小时周期。
在分支误用的情况下,浪费的时间相当于从取货阶段到执行阶段的输油管阶段的数量。 现代微处理器往往有相当长的输油管,因此误用延迟时间在10到20小时之间。 结果,输油管更长时间增加了对更先进的分支预测器的需求。
如你所见,我们似乎没有理由不使用 部门预言家。
这是一个很简单的演示, 澄清了分支预测器的基本部分。 如果这些 gifs 令人烦恼, 请随意将其从答案中删除, 访问者也可以从 PredictorDemo 获得现场演示源代码 。
是关于分支预测的 是什么?
分支预测器是一种古老的改进性能的技术,在现代建筑中仍然具有相关性。 虽然简单的预测技术提供了快速的外观和电力效率,但它们受到高误用率的影响。 另一方面,复杂的分支预测 — — 无论是基于神经的预测还是两级分支预测的变体 — — 提供了更好的预测准确性,但是它们消耗的动力和复杂性也成倍增加。 此外,在复杂的预测技术中,预测分支所需的时间也非常长 — — 从2到5个周期不等 — — 这与实际分支的执行时间相当。 分支预测基本上是一个优化(最小化 ) 问题, 重点是实现尽可能最低的误用率、低电耗和最低资源低的复杂度。
确实有三种不同的分支:
附加条件的前进分支 - 根据运行时间条件, PC (程序反) 被修改为指示流中前方的地址 。
后向有条件的分支 - PC 被修改为指令流中的后向点。 分支基于某些条件, 例如当循环结尾的测试显示循环应该再次执行时, 将程序循环的起始处向后转至程序循环的起始处 。
无条件分支 - 包括跳跃、 程序电话和返回等没有特定条件的无条件跳跃指令。 例如, 无条件跳跃指令可能会以组合语言编码为简单的“ jmp ” , 且指令流必须立即被指向跳跃指令指向的目标位置, 而有条件跳跃可能以“ jmpne ” 编码为“ jmpne ” , 只有在对先前“ 比较” 指令中两个数值进行比较的结果显示数值不相等时, 才会改变教学流的方向 。 ( x86 结构使用的分段处理方案增加了额外的复杂性, 因为跳跃可以是“ 接近” (在一段内) , 也可以是“ far”(在段外) 。 每种类型对分支预测算法都有不同的效果 。
静态/动态分支预测:微处理器在第一次遇到有条件的分支时使用静态分支预测,在随后执行有条件的分支代码时使用动态分支预测。
参考文献:
预测处预测处(Ussing回背机器)
在同一行中(我认为没有任何答案强调这一点),最好提到有时(特别是在软件中,在软件中,性能很重要——如Linux内核),如果声明如下,你可以找到一些:
if (likely( everything_is_ok ))
{
/* Do something */
}
或类似:
if (unlikely(very_improbable_condition))
{
/* Do something */
}
可能性 () 和 可能性 () 实际上都是宏, 其定义是使用海合会的 ` 内建_ 期望 ' 来帮助编译者插入预测代码, 以考虑到用户提供的信息, 从而有利于该条件。 海合会支持其他能够改变运行程序的行为或发布低级别指令, 如清除缓存等 。 请参见此文档, 内容可以通过海合会的现有内建 。
通常这种优化主要在硬实时应用程序或内嵌系统中找到,在这些系统中,执行时间很重要且至关重要。例如,如果您正在检查某些错误条件,而错误条件只发生1/10000 000次,那么为什么不通知编译者?这样,默认情况下,分支预测会假设该条件是假的。
当对数组进行排序时,数据在 0 至 255 之间分布,因此,约前半段的迭代将不输入 " 如果 " 报表(如果在下文中共享语句)。
if (data[c] >= 128)
sum += data[c];
问题是: 是什么使上述语句在某些情况下无法执行, 如分类数据那样? 这里出现了“ 分支预测器 ” 。 分支预测器是一个数字电路, 试图猜出分支( 如当日电子结构 ) 将走哪条路, 然后再确定这一点。 分支预测器的目的是改善教学管道的流量 。 分支预测器在实现高效运行方面发挥着关键作用 !
让我们做一些板凳标记 来更好理解它
如果情况总是真实的,或者总是虚假的,处理器中的分支预测逻辑会抓住这个模式。 另一方面,如果情况无法预测,那么如果情况说明会更昂贵。
让我们用不同的条件来衡量这个循环的性能:
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
“坏”真实假象模式可以使虚报速度比“好”模式慢六倍! 当然,哪种模式是好的,哪一种模式不好,取决于汇编者产生的准确指示和具体的处理器。
因此,部门预测对业绩的影响是毫无疑问的!
如果您对这个代码可以做的更多优化感到好奇, 请考虑 :
以原始循环开始 :
for (unsigned i = 0; i < 100000; ++i)
{
for (unsigned j = 0; j < arraySize; ++j)
{
if (data[j] >= 128)
sum += data[j];
}
}
通过循环互换,我们可以安全地将这一循环改为:
for (unsigned j = 0; j < arraySize; ++j)
{
for (unsigned i = 0; i < 100000; ++i)
{
if (data[j] >= 128)
sum += data[j];
}
}
然后,你可以看到,如果条件是不变的 在整个执行 i 循环, 所以你可以拉起,如果:
for (unsigned j = 0; j < arraySize; ++j)
{
if (data[j] >= 128)
{
for (unsigned i = 0; i < 100000; ++i)
{
sum += data[j];
}
}
}
然后,你看,内环会崩溃成一个单一的表达式, 假设浮点模型允许它(/ fp: fast 被丢弃, 例如)
for (unsigned j = 0; j < arraySize; ++j)
{
if (data[j] >= 128)
{
sum += data[j] * 100000;
}
}
这比以前快了十万倍