c++ - 用于小型未对齐数据的快速 memcpy

标签 c++ performance memory

我需要读取一个由许多基本类型组成的二进制文件,例如 int、double、UTF8 字符串等。例如,考虑一个包含 n 对 (int, double) 的文件,一个接一个,没有与 n 的任何对齐都在数千万级。我需要非常快速地访问该文件。我使用 fread 调用和我自己的大约 16 kB 长的缓冲区读取文件。

分析器显示我的主要瓶颈恰好是从内存缓冲区复制到其最终目的地。编写从缓冲区复制到 double 的函数的最明显方法是:

// x: a pointer to the final destination of the data
// p: a pointer to the buffer used to read the file
//
void f0(double* x, const unsigned char* p) {
  unsigned char* q = reinterpret_cast<unsigned char*>(x);
  for (int i = 0; i < 8; ++i) {
    q[i] = p[i];
  }
}

如果我使用下面的代码,我在 x86-64 上获得了巨大的加速

void f1(double* x, const unsigned char* p) {
  double* r = reinterpret_cast<const double*>(p);
  *x = *r;
}

但是,据我所知,如果 p 不是 8 字节对齐的,程序将在 ARM 上崩溃。

这是我的问题:

  • 第二个程序是否保证可以在 x86 和 x86-64 上运行?
  • 如果您需要尽快在 ARM 上编写这样的函数,您将如何编写?

这是一个在你的机器上测试的小基准

#include <chrono>
#include <iostream>

void copy_int_0(int* x, const unsigned char* p) {
  unsigned char* q = reinterpret_cast<unsigned char*>(x);
  for (std::size_t i = 0; i < 4; ++i) {
    q[i] = p[i];
  }
}

void copy_double_0(double* x, const unsigned char* p) {
  unsigned char* q = reinterpret_cast<unsigned char*>(x);
  for (std::size_t i = 0; i < 8; ++i) {
    q[i] = p[i];
  }
}

void copy_int_1(int* x, const unsigned char* p) {
  *x = *reinterpret_cast<const int*>(p);
}

void copy_double_1(double* x, const unsigned char* p) {
  *x = *reinterpret_cast<const double*>(p);
}

int main() {
  const std::size_t n = 10000000;
  const std::size_t nb_times = 200;
  unsigned char* p = new unsigned char[12 * n];
  for (std::size_t i = 0; i < 12 * n; ++i) {
    p[i] = 0;
  }
  int* q0 = new int[n];
  for (std::size_t i = 0; i < n; ++i) {
    q0[i] = 0;
  }
  double* q1 = new double[n];
  for (std::size_t i = 0; i < n; ++i) {
    q1[i] = 0.0;
  }

  const auto begin_0 = std::chrono::high_resolution_clock::now();
  for (std::size_t k = 0; k < nb_times; ++k) {
    for (std::size_t i = 0; i < n; ++i) {
      copy_int_0(q0 + i, p + 12 * i);
      copy_double_0(q1 + i, p + 4 + 12 * i);
    }
  }
  const auto end_0 = std::chrono::high_resolution_clock::now();
  const double time_0 =
      1.0e-9 *
      std::chrono::duration_cast<std::chrono::nanoseconds>(end_0 - begin_0)
          .count();
  std::cout << "Time 0: " << time_0 << " s" << std::endl;

  const auto begin_1 = std::chrono::high_resolution_clock::now();
  for (std::size_t k = 0; k < nb_times; ++k) {
    for (std::size_t i = 0; i < n; ++i) {
      copy_int_1(q0 + i, p + 12 * i);
      copy_double_1(q1 + i, p + 4 + 12 * i);
    }
  }
  const auto end_1 = std::chrono::high_resolution_clock::now();
  const double time_1 =
      1.0e-9 *
      std::chrono::duration_cast<std::chrono::nanoseconds>(end_1 - begin_1)
          .count();
  std::cout << "Time 1: " << time_1 << " s" << std::endl;
  std::cout << "Prevent optimization: " << q0[0] << " " << q1[0] << std::endl;

  delete[] q1;
  delete[] q0;
  delete[] p;

  return 0;
}

我得到的结果是

clang++ -std=c++11 -O3 -march=native copy.cpp -o copy
./copy
Time 0: 8.49403 s
Time 1: 4.01617 s

g++ -std=c++11 -O3 -march=native copy.cpp -o copy
./copy
Time 0: 8.65762 s
Time 1: 3.89979 s

icpc -std=c++11 -O3 -xHost copy.cpp -o copy
./copy
Time 0: 8.46155 s
Time 1: 0.0278496 s

我还没有检查汇编,但我猜英特尔编译器在这里欺骗了我的基准。

最佳答案

Is the second program guaranteed to work on both x86 and x86-64?

没有。

当您取消引用 double* 时,编译器 is free to assume内存位置实际上包含一个 double,这意味着它必须与 alignof(double) 对齐。

很多 x86 指令都可以安全地用于未对齐的数据,但不是全部。具体来说,有一些 SIMD 指令需要正确对齐,您的编译器可以免费使用这些指令。

这不仅仅是理论上的; LZ4 曾经使用与您发布的非常相似的东西(它是 C,而不是 C++,所以它是 C 风格的转换而不是 reinterpret_cast,但这并不重要),并且一切都按预期工作.然后发布了 GCC 5,并且 it auto-vectorized the code in question在 -O3 使用 vmovdqa,这需要正确对齐。最终结果是,在 GCC ≤ 4.9 中运行良好的代码在使用 GCC ≥ 5 编译时开始在运行时崩溃。

换句话说,即使您的程序今天碰巧可以运行,如果您依赖于未对齐的访问(或其他未定义的行为),明天它也很容易停止运行。不要这样做。

How would you write such a function on ARM if you need it as fast as you can?

答案并不是特定于 ARM 的。在 LZ4 事件之后 Yann Collet(LZ4 的作者)做了 a lot of research来回答这个问题。没有一种选项可以很好地为每种架构上的每个编译器生成最佳代码。

使用memcpy() 是最安全的选择。如果在编译时已知大小,编译器通常会优化 memcpy() 调用......对于更大的缓冲区,您可以通过调用 memcpy() 来利用它一个循环;您通常会得到一个快速指令循环,而无需调用 memcpy() 的额外开销。

如果您喜欢冒险,可以使用压缩 union 来“转换”而不是 reinterpret_cast。这是特定于编译器的,但如果支持它应该是安全的,并且它可能memcpy() 更快。​​

FWIW,我有 some code它试图根据各种因素(编译器、编译器版本、体系结构等)找到执行此操作的最佳方法。对于我没有测试过的平台来说有点保守,但在人们实际使用的绝大多数平台上应该会取得不错的效果。

关于c++ - 用于小型未对齐数据的快速 memcpy,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48312068/

相关文章:

c# - 查找大于 double 值的最小 float

C++向下转换以撤消函数覆盖

c++ - 没有临时指针的链初始化

c - 如何访问存储在 char 数组中的整数?

javascript - 从 JavaScript 中的局部变量释放内存

c++ - 如何释放重新分配的内存? C++

python - 高效的字节查找表

python - 基于 Pandas 组内日期的有效转换?

c++ - 以多态方式处理非多态对象,没有性能开销

c - 内存分配器建议