用于小未对齐数据的快速内存
Fast memcpy for small unaligned data
我需要读取一个二进制文件,该文件由许多基本类型组成,例如int,double,UTF8字符串等。例如,考虑一个文件一个接一个地包含 n 对 (int, double),与 n 没有任何对齐,大约是数千万。我需要非常快速地访问该文件。我使用fread
调用和我自己的缓冲区读取文件,该缓冲区长约 16 kB。
探查器显示我的主要瓶颈恰好是从内存缓冲区复制到其最终目标。编写从缓冲区复制到双精度的函数的最明显方法是:
// 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
我还没有检查程序集,但我想英特尔编译器在这里愚弄了我的基准测试。
第二个程序是否保证在 x86 和 x86-64 上运行?
不。
当您取消引用double*
编译器可以自由地假定内存位置实际上包含一个双精度值,这意味着它必须与alignof(double)
对齐。
许多 x86 指令可以安全地用于未对齐的数据,但并非所有指令都是如此。具体来说,有些 SIMD 指令需要正确对齐,您的编译器可以免费使用这些指令。
这不仅仅是理论上的;LZ4 曾经使用与您发布的内容非常相似的东西(它是 C,而不是 C++,所以它是一个 C 风格的演员表而不是reinterpret_cast
,但这并不重要),一切都按预期工作。然后GCC 5被发布,它使用vmovdqa
在-O3处自动矢量化有问题的代码,这需要正确的对齐。最终结果是,在使用 GCC ≥≤ 5 编译时,在 GCC 4.9 中运行良好的代码在运行时开始崩溃。
换句话说,即使您的程序碰巧在今天工作,如果您依赖于未对齐的访问(或其他未定义的行为),它明天也很容易停止工作。别这样。
如果你需要尽可能快地在ARM上编写这样的函数,你会怎么写呢?
答案并不是真正特定于 ARM 的。LZ4事件后,Yann Collet(LZ4的作者)做了大量的研究来回答这个问题。没有一个选项可以很好地为每个架构上的每个编译器生成最佳代码。
使用memcpy()
是最安全的选择。如果在编译时知道大小,编译器通常会优化memcpy()
调用...对于较大的缓冲区,可以通过循环调用memcpy()
来利用这一点;您通常会得到一个快速指令循环,而不会产生调用memcpy()
的额外开销。
如果你觉得更冒险,你可以使用一个打包的工会来"投掷"而不是reinterpret_cast
。这是特定于编译器的,但是当支持时,它应该是安全的,并且可能比memcpy()
更快。
FWIW,我有一些代码试图根据各种因素(编译器、编译器版本、架构等)找到最佳方法。对于我没有测试过的平台,它有点保守,但它应该在人们实际使用的绝大多数平台上取得良好的效果。
- 在C++中打印指向不同基元数据类型的指针的内存地址
- 如何在 malloc 内存中初始化非 POD 数据
- 如何使用 MPI 的远程内存访问 (RMA) 功能并行化数据聚合?
- 为什么字符串的 move() 会改变内存中底层数据的位置?
- C++,您能否设计一种数据结构,将指针保存在连续内存中并且不会使它们失效?
- 存储在哪个内存段(代码/数据段)类(员工)中?
- 基于浅树的数据结构的内存分配器,用于频繁分配和解除分配
- 获取 R 数据帧的内存地址
- 将数据存储在内存中以供以后访问
- 整数数据如何以位为单位存储在内存中?不是右对齐吗?
- 数据在内存 c++ 中丢失
- 我可以使用哪种数据结构来释放连续内存中的内存?
- 使用 delete [] 运算符取消分配类中数据成员的内存
- 为什么 std::set 容器使用的内存比其数据大小多得多?
- 如果一个变量在它之前释放了另一个(相同的数据类型)变量,如何将其分配给内存?
- 内存映射C++中的流数据
- 如何返回定义良好的内存部分?例如来自图像数据的像素的颜色值
- C++来自课堂的内存数据?
- Libjpeg将图像写入内存数据
- 将内存数据PTR映射到qt输入字段