这是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);
}
}
其结果类似,但不太极端。
我第一种想法是 分类能把数据带进缓存缓存,但那是愚蠢的 因为阵列是刚刚产生的。
- 这是怎么回事?
- 为什么处理一个分类阵列的速度要快于处理一个未排序阵列的速度?
守则正在总结一些独立的术语,因此命令不应重要。
相关/后续行动不同/以后的编译者和选项的相同效果:
除了树枝预测可能会减慢你的速度之外 分解阵列还有另一个优势
您可以有一个停止状态, 而不是仅仅检查值, 这样你只能环绕相关数据, 忽略其它数据 。
分支预测只会错失一次。
// 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];
}
其他答复的假设是,一个人需要对数据进行分类是不正确的。
以下代码不排序整个阵列,但只排序其中的200个元素部分,因此运行速度最快。
只对 K 元素部分进行排序,以线性时间完成预处理,O(n)
,而不是O(n.log(n))
排序整个阵列需要时间 。
#include <algorithm>
#include <ctime>
#include <iostream>
int main() {
int data[32768]; const int l = sizeof data / sizeof data[0];
for (unsigned c = 0; c < l; ++c)
data[c] = std::rand() % 256;
// sort 200-element segments, not the whole array
for (unsigned c = 0; c + 200 <= l; c += 200)
std::sort(&data[c], &data[c + 200]);
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i) {
for (unsigned c = 0; c < sizeof data / sizeof(int); ++c) {
if (data[c] >= 128)
sum += data[c];
}
}
std::cout << static_cast<double>(clock() - start) / CLOCKS_PER_SEC << std::endl;
std::cout << "sum = " << sum << std::endl;
}
这个“证明”也与任何算法问题无关, 比如排序顺序, 并且确实是分支预测。
我用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几乎是175乘175次低于 C 执行的慢于 C 执行,没有排序和350乘350次换句话说,其效果(分支预测)是:1.46x执行和2.7x执行《公约》的《公约》。
正如其他人已经提到的,神秘背后的背后是什么?处 预测员.
我不是要补充一些东西,而是要用另一种方式解释这个概念。维基文字有一个简明的介绍,里面有文字和图表。我确实喜欢下面的解释,下面用一个图表来用直觉来描述处的预言。
在计算机结构中,分支预测器是一种数字电路,它试图猜到分支(如如果是当时的else结构)将走哪条路,然后才能确定这一点。分支预测器的目的是改善教学管道的流量。分支预测器在很多现代管道式微处理器结构(如x86)实现高效运行方面发挥着关键作用。
双向分机通常是用有条件跳跃指令执行的。 有条件跳跃要么可以“ 不采取” , 继续使用在有条件跳跃后立即出现的代码第一分支, 要么可以在存储代码第二分支的方案记忆中“ 采取” 并跳到不同位置。 无法确定在计算条件和有条件跳跃通过指令管道的执行阶段之前是否采取有条件跳跃(见图1)。
根据所述情况,我写了动画演示,以显示在不同情况下如何在管道中执行指示。
- 没有部门预言家。
没有分支预测,处理器必须等到有条件跳跃指令通过执行阶段后,下一个指令才能进入管道的接货阶段。
该示例包含三个指令, 第一个是有条件跳跃指令。 后两个指令可以进入管道, 直到有条件跳跃指令执行为止 。
完成3项指示需要9小时周期。
- 使用预测器,不要采取有条件的跳跃。让我们假设预测是否接受有条件的跳跃。
完成3项指示需要7小时周期。
- 使用预测器 进行有条件的跳跃 假设预测是否接受有条件的跳跃。
完成3项指示需要9小时周期。
在分支误用的情况下,浪费的时间相当于从取货阶段到执行阶段的输油管阶段的数量。 现代微处理器往往有相当长的输油管,因此误用延迟时间在10到20小时之间。 结果,输油管更长时间增加了对更先进的分支预测器的需求。
如你所见,我们似乎没有理由不使用 部门预言家。
这是一个很简单的演示,可以澄清分支预言家的基本部分。如果这些小精灵很烦人,请随意将他们从答案中删除,访问者也可以从中获取源代码。PrepdictorDemo 分支介质
如果您对这个代码可以做的更多优化感到好奇, 请考虑 :
以原始循环开始 :
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];
}
}
然后,你可以看到,if
条件条件在始终执行时为常数。i
循环,这样你就可以升起if
外出 :
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;
}
}
这比以前快了十万倍
快速和简单理解的答案(阅读其他细节)
这一概念被称为子分支预测
分支预测是一种优化技术,它预言代码在被确知之前将走的道路。 这一点很重要,因为在代码执行过程中,机器预设了几条代码声明并将其储存在管道中。
问题出在有条件的分支中,有两种可能的路径或代码部分可以执行。
当预测是真实的, 优化技术 完成。
当预测是虚假的,用简单的方式解释, 管道中储存的代码声明被证明是错误的, 而实际的代码必须全部重新加载, 这需要很多时间。
正如常识所显示的,对某类物品的预测比对某类未分类物品的预测更准确。
分支预测可视化:
已分类
未排序