【问题标题】:Sorting 32 bit ints is as fast as sorting 64 bit ints排序 32 位整数与排序 64 位整数一样快
【发布时间】:2016-08-19 20:24:54
【问题描述】:

以下是我认为是事实的事情:

  • 快速排序应该对缓存非常友好。
  • 一个 64 字节的高速缓存行可以包含 16 个 32 位整数或 8 个 64 位整数。

假设:

  • 对 32 位整数向量进行排序应该比对 64 位整数向量。

但是当我运行下面的代码时,我得到了结果:

i16 = 7.5168 
i32 = 7.3762 
i64 = 7.5758

为什么我没有得到我想要的结果?

C++:

#include <iostream> 
#include <vector>
#include <cstdint>
#include <algorithm>
#include <chrono>


int main() {
  const int vlength = 100'000'000;
  const int maxI = 50'000;

  std::vector<int16_t> v16;
  for (int i = 0; i < vlength; ++i) {
    v16.push_back(int16_t(i%maxI));
  }
  std::random_shuffle(std::begin(v16), std::end(v16));
  std::vector<int32_t> v32;
  std::vector<int64_t> v64;
  for (int i = 0; i < vlength; ++i) {
    v32.push_back(int32_t(v16[i]));
    v64.push_back(int64_t(v16[i]));
  }

  auto t1 = std::chrono::high_resolution_clock::now();
  std::sort(std::begin(v16), std::end(v16));
  auto t2 = std::chrono::high_resolution_clock::now();
  std :: cout << "i16 = " << (std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1)).count() << std :: endl;

  t1 = std::chrono::high_resolution_clock::now();
  std::sort(std::begin(v32), std::end(v32));
  t2 = std::chrono::high_resolution_clock::now();
  std :: cout << "i32 = " << (std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1)).count() << std :: endl;

  t1 = std::chrono::high_resolution_clock::now();
  std::sort(std::begin(v64), std::end(v64));
  t2 = std::chrono::high_resolution_clock::now();
  std :: cout << "i64 = " << (std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1)).count() << std :: endl;

}

编辑: 为了避免缓存友好排序的问题,我还尝试了以下代码:

template <typename T>
inline void function_speed(T& vec) {
  for (auto& i : vec) {
    ++i;
  }
}

int main() {
  const int nIter = 1000;

  std::vector<int16_t> v16(1000000);
  std::vector<int32_t> v32(1000000);
  std::vector<int64_t> v64(1000000);



  auto t1 = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < nIter; ++i) {
    function_speed(v16);
  }
  auto t2 = std::chrono::high_resolution_clock::now();
  std :: cout << "i16 = " << (std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1)).count()/double(nIter) << std :: endl;

  t1 = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < nIter; ++i) {
    function_speed(v32);
  }
  t2 = std::chrono::high_resolution_clock::now();
  std :: cout << "i32 = " << (std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1)).count()/double(nIter) << std :: endl;

  t1 = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < nIter; ++i) {
    function_speed(v64);
  }
  t2 = std::chrono::high_resolution_clock::now();
  std :: cout << "i64 = " << (std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1)).count()/double(nIter) << std :: endl;

}

典型结果:

i16 = 0.00618648
i32 = 0.00617911
i64 = 0.00606275

我知道正确的基准测试本身就是一门科学,也许我做错了。

EDIT2: 通过避免溢出,我现在开始得到更有趣的结果:

template <typename T>
inline void function_speed(T& vec) {
  for (auto& i : vec) {
    ++i;
    i %= 1000;
  }
}

给出如下结果:

i16 = 0.0143789
i32 = 0.00958941
i64 = 0.019691

如果我这样做:

template <typename T>
inline void function_speed(T& vec) {
  for (auto& i : vec) {
    i = (i+1)%1000;
  }
}

我明白了:

i16 = 0.00939448
i32 = 0.00913768
i64 = 0.019615

【问题讨论】:

  • 7.5,是秒吗?还是毫秒?
  • 您期望哪个差异(因素)?
  • @Jean-FrançoisFabre:秒。
  • @Jarod42:我不确定我预期的差异有多大,但至少有些明显。
  • 顺便说一下,当您尝试将数字 &gt;= 32768 存储在 16 位有符号整数中时会溢出。但这不应该改变任何基准,因为溢出的值会按原样复制到其他向量。

标签: c++ caching memory int


【解决方案1】:

错误的假设;所有 O(N log N) 排序算法必须对绝大多数 N 中的缓存不友好!可能的输入。

此外,我认为优化编译器可以彻底消除排序,未优化的构建当然对基准测试毫无意义。

【讨论】:

  • 我一直认为合并排序对缓存友好,只要您至少有一个 3 路缓存。
  • @MarkRansom:最后,在合并大序列时。当在单个缓存行上操作时,早期的东西也不错(尽管您可能不会使用合并排序到最后 2 个元素;相反,您会为缓存行级别使用专用的排序器)。不过,中间部分看起来相当糟糕,我认为您仍然会有 O(N log N) 缓存未命中。预取器可能会有所帮助。
  • 我不认为缓存不友好是缺乏性能差异的原因。缓存不友好保证您必须对所有三种 int 大小进行 O(n log n) 内存访问。 但是在 int64 情况下读取的字节数仍然是 int16 情况下的 4 倍。 因此,如果这是内存绑定算法,那么基准测试中应该有 4 倍的速度差异。
  • 其实我只是想了 3 秒,才意识到在 int16 的情况下,即使每次内存获取每次比较都会抓取 2 个字节的有用数据,但架构仍然会拉入周围的 62 个字节,因此无论 int 中的位数如何,每次内存获取都是 64 字节。
  • @NicuStiurca:正确,而且排序算法如何很好地使用这些附近的值是相当关键的。问题是,平均而言,附近的输入值在输出中结束半个数组。您只需要进行这么多 12 字节的移动,而这里您需要进行超过 100MB 距离的许多移动。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-19
  • 1970-01-01
  • 2019-03-25
  • 1970-01-01
  • 2011-09-12
  • 1970-01-01
  • 2019-08-17
相关资源
最近更新 更多