跳转至

HPC 中的 C/C++

约 600 个字 39 行代码 预计阅读时间 2 分钟

在这一部分中,我们将介绍一些在 HPC 中常用的 C/C++ 编程技巧和实践。这些技巧通常与底层硬件平台高度相关,使用时应注意平台特性。

内存对齐

内存对齐是指将数据对齐至缓存行(通常为 64B)或更大尺度(如 4K内存页),以提高访问速度和效率。在 HPC 中,内存对齐可以显著提高程序进行内存访问的速度,进而提示程序性能。以下是一些常见的内存对齐技巧:

默认情况,基本数据类型(如 intdouble 等)通常会自动对齐到其大小,而结构体(不论是 struct 还是 class)的对齐方式取决于其成员对齐方式的最大值,因此结构体的大小往往大于其成员大小之和。

struct S1 {   // 整体对齐到 8 字节, sizeof(S1) = 16
    char c;   // `c` 会对齐到 1 字节。
    int i;    // `i` 会对齐到 4 字节。
    double d; // `d` 会对齐到 8 字节。
};

对于需要手动指定变量对齐的情况,可以使用 alignas 关键字(C++11 起)或__attribute__((aligned(64)))(仅限与 GCC 兼容的编译器)来指定变量或类型的对齐方式。具体用法详见 cppreference,以下是一些示例:

alignas(64) int x; // `x` 在内存中对齐到64字节。
struct alignas(64) S2 { // `S` 整体在内存中对齐到64字节。
    double x;
    double y;
};
S2 s[2]; // `s` 数组中的每个元素都对齐到64字节,因此实际占用的内存为 128 字节。
alignas(64) int y[2]; // `y` 数组中的首个元素对齐到64字节,因此占用空间仍然为8字节

对于在堆上动态分配内存的情况,可以使用 aligned_alloc (C++17 起,C11 起) 来分配对齐的内存。

#include <cstdlib> // C++17 起
#include <stdlib.h> // C11 起
int* p = aligned_alloc(64, sizeof(int) * 100);
assert(reinterpret_cast<uintptr_t>(p) % 64 == 0); // p 保证对齐到 64 字节

数据局部性优化

数据局部性优化的原理是 CPU 在程序访问内存的时候,往往会自动将临近的内存数据加载到缓存中,从而如果接下来访问这些数据就会变得更快。为了充分利用 CPU 的这一特性,我们可以通过以下方式优化数据局部性:

数据存储方式优化

根据运算场景不同,我们可以灵活选取使用结构体数组 (AoS) 或 数组结构体 (SoA)

struct Point1 {
    float x, y, z;
};
Point1 points1[1000]; // 结构体数组 (AoS)

struct Point2 {
    float x[1000];
    float y[1000];
    float z[1000];
};
Point2 points2; // 数组结构体 (SoA)

通常而言,如果需要频繁访问同一结构体的所有成员,使用结构体数组 (AoS) 更加高效;如果仅访问某一特定成员,则使用数组结构体 (SoA) 更加高效。

数据访问模式优化

对于固定存储方式的数据,我们还可以通过改变访问顺序来提高数据局部性,常见的一个手法便是循环分块(Loop Blocking)。

// 原始循环
for (int i = 0; i < N; i++)
  for (int j = 0; j < N; j++)
    A[i][j] = B[i][j] * C[i][j];

// 分块优化(块大小BLOCK)
constexpr int BLOCK = 64; // 假设取块大小 64
for (int ii = 0; ii < N; ii += BLOCK)
  for (int jj = 0; jj < N; jj += BLOCK)
    for (int i = ii; i < ii + BLOCK; i++)
      for (int j = jj; j < jj + BLOCK; j++)
        A[i][j] = B[i][j] * C[i][j];

数据访问模式有时候也有利于编译器进行自动向量化(Auto-vectorization),从而进一步提高性能。