【发布时间】:2023-11-11 04:36:01
【问题描述】:
这是一段 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 秒。
(排序本身比遍历数组需要更多时间,因此如果我们需要为未知数组计算它,实际上并不值得这样做。)
最初,我认为这可能只是语言或编译器异常,所以我尝试了 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);
}
}
具有相似但不那么极端的结果。
我的第一个想法是排序将数据带入cache,但后来我认为这是多么愚蠢,因为数组刚刚生成。
- 这是怎么回事?
- 为什么处理排序数组比处理未排序数组更快?
代码总结了一些独立的术语,所以顺序应该无关紧要。
相关/后续问答关于不同/以后的编译器和选项的相同效果:
【问题讨论】:
-
为了记录,您的数据不需要排序,只需partitioned,这是一个更快的操作。
-
另一个观察是你不需要对数组进行排序,但你只需要使用值 128 对其进行分区。排序是 n*log(n),而分区只是线性的。基本上,这只是快速排序分区步骤的一次运行,其中枢轴选择为 128。不幸的是,在 C++ 中只有 nth_element 函数,它按位置分区,而不是按值分区。
-
@screwnut 这是一个实验,它表明分区就足够了:创建一个未排序但分区的数组,其中包含其他随机内容。测量时间。把它分类。再次测量时间。这两个测量值应该基本上无法区分。 (实验 2:创建一个随机数组。测量时间。对其进行分区。再次测量时间。您应该看到与排序相同的加速。您可以将两个实验合二为一。)
-
顺便说一句。在 Apple M1 上,未排序的代码在 17 秒内运行,排序后的代码在 7 秒内运行,因此分支预测惩罚在 RISC 架构上并没有那么糟糕。
-
@RomanYavorskyi:这取决于编译器。如果他们为此特定测试制作无分支汇编(例如,作为Why is processing an unsorted array the same speed as processing a sorted array with modern x86-64 clang? 中的SIMD 向量化的一部分,或者只是使用标量
cmov(gcc optimization flag -O3 makes code slower than -O2),那么排序与否无关紧要。但不可预测的分支仍然存在当它不像计数那么简单时,这是一件非常真实的事情,所以删除这个问题会很疯狂。
标签: java c++ performance cpu-architecture branch-prediction