内存对齐与典型内存布局优化¶
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <new>
// 打印指针、大小和对齐信息
void printAlignmentInfo(const char* name, const void* ptr,
std::size_t size_of_type, std::size_t align_of_type) {
auto address = reinterpret_cast<std::uintptr_t>(ptr);
std::cout << "--- " << name << " ---\n";
std::cout << " 地址 (Address) : 0x" << std::hex << address << std::dec
<< "\n";
std::cout << " sizeof : " << size_of_type << " 字节\n";
std::cout << " alignof : " << align_of_type << " 字节\n";
if (address % align_of_type == 0) {
std::cout << " 对齐检查 : 通过\n";
} else {
std::cout << " 对齐检查 : 失败\n";
}
}
// 1. 默认struct对齐与内存填充
struct DefaultAlignedStruct {
char a; // 1 byte
// 3 bytes padding
int b; // 4 bytes
double c; // 8 bytes
}; // sizeof = 16, alignof = 8
void demonstrate_default_alignment() {
DefaultAlignedStruct instance;
printAlignmentInfo("1. DefaultAlignedStruct", &instance, sizeof(instance),
alignof(instance));
}
// 2. `alignas`: 编译时指定对齐
struct alignas(32) AlignasType {
int data[5]; // 5 * 4 = 20 bytes. 总大小补齐至32字节。
};
void demonstrate_alignas() {
AlignasType instance;
printAlignmentInfo("2. AlignasType", &instance, sizeof(instance),
alignof(instance));
alignas(64) char buffer[100];
printAlignmentInfo("2. alignas(64) buffer", buffer, sizeof(buffer),
alignof(buffer));
}
// 3. `std::aligned_alloc`: 动态分配对齐的原始内存
void demonstrate_aligned_alloc() {
const size_t alignment = 64;
const size_t requested_size = 200;
// `aligned_alloc` 要求 size 必须是 alignment 的整数倍
size_t size_to_alloc =
((requested_size + alignment - 1) / alignment) * alignment;
void* mem = std::aligned_alloc(alignment, size_to_alloc);
if (!mem) {
return;
}
printAlignmentInfo("3. std::aligned_alloc", mem, size_to_alloc, alignment);
// 使用 std::free() 释放
std::free(mem);
}
// 4. 带对齐的 `new`: 动态创建对齐的对象
class AlignedNewObject {
public:
AlignedNewObject() { std::cout << " -> AlignedNewObject 构造\n"; }
~AlignedNewObject() { std::cout << " -> AlignedNewObject 析构\n"; }
private:
alignas(16) float simd_data[4];
};
void demonstrate_aligned_new() {
const size_t alignment = 128;
auto* obj =
new (std::align_val_t{alignment}) AlignedNewObject(); // C++17标准
printAlignmentInfo("4. AlignedNewObject (aligned new)", obj, sizeof(*obj),
alignof(*obj));
delete obj;
}
// 5. `#pragma pack`: 紧凑内存布局
#pragma pack(push, 1) // 设置1字节对齐
struct PackedStruct {
char a; // 1 byte
int b; // 4 bytes
double c; // 8 bytes
}; // sizeof = 1+4+8=13, alignof = 1
#pragma pack(pop) // 恢复默认对齐
void demonstrate_packing() {
PackedStruct instance;
printAlignmentInfo("5. PackedStruct (#pragma pack(1))", &instance,
sizeof(instance), alignof(instance));
// 禁用填充可能影响性能且不可移植。
}
int main() {
demonstrate_default_alignment();
std::cout << "\n";
demonstrate_alignas();
std::cout << "\n";
demonstrate_aligned_alloc();
std::cout << "\n";
demonstrate_aligned_new();
std::cout << "\n";
demonstrate_packing();
return 0;
}
#pragma once
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <new>
#include <type_traits>
#if defined(__linux__)
#include <sys/mman.h>
#endif
/**
* @brief An STL-compliant allocator for high-performance scenarios.
*
* Allocates small blocks (<= 16KB) with 64-byte alignment.
* Allocates large blocks (> 16KB) with 2MB alignment and advises for huge pages
* on Linux. All memory is zero-initialized. Stateless as required by the
* standard.
*
* @tparam T The type of value to allocate memory for.
*/
template <typename T>
requires std::is_trivial_v<T>
struct AlignedAllocator {
using value_type = T;
AlignedAllocator() noexcept = default;
template <class U>
constexpr AlignedAllocator(const AlignedAllocator<U>&) noexcept {}
[[nodiscard]] T* allocate(size_t n) {
if (n > static_cast<size_t>(-1) / sizeof(T)) {
throw std::bad_alloc();
}
size_t num_bytes = n * sizeof(T);
void* ptr = nullptr;
if (num_bytes <= (1 << 14)) { // 16KB threshold
const size_t alignment = 64;
size_t sz = (num_bytes + alignment - 1) & ~(alignment - 1);
ptr = std::aligned_alloc(alignment, sz);
if (ptr) {
std::memset(ptr, 0, sz);
}
} else { // Large allocation path
const size_t alignment = 1 << 21; // 2MB
size_t sz = (num_bytes + alignment - 1) & ~(alignment - 1);
ptr = std::aligned_alloc(alignment, sz);
if (ptr) {
#if defined(__linux__)
madvise(ptr, sz, MADV_HUGEPAGE);
#endif
std::memset(ptr, 0, sz);
}
}
if (!ptr) {
throw std::bad_alloc();
}
return static_cast<T*>(ptr);
}
void deallocate(T* p, size_t /*n*/) noexcept { std::free(p); }
// Rebind structure for allocator compatibility
template <typename U>
struct rebind {
using other = AlignedAllocator<U>;
};
};
// Equality operators for stateless allocators
template <class T, class U>
bool operator==(const AlignedAllocator<T>&, const AlignedAllocator<U>&) {
return true;
}
template <class T, class U>
bool operator!=(const AlignedAllocator<T>&, const AlignedAllocator<U>&) {
return false;
}
1.根据运算场景,灵活选取使用结构体数组 (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) 更加高效。
// 使用 Struct of Arrays (SoA) 数据结构
struct EmployeeData {
std::vector<std::string> names;
std::vector<double> salaries;
std::vector<double> taxes;
EmployeeData(size_t capacity) {
names.reserve(capacity);
salaries.reserve(capacity);
taxes.resize(capacity);
}
void add_employee(const std::string& name, double salary) {
names.push_back(name);
salaries.push_back(salary);
}
size_t size() const { return names.size(); }
};
2. 去规范化的数据结构
// --- 规范化设计 (Normalized Design) ---
struct RiskProfile {
uint32_t max_order_size;
double max_position_value;
};
struct Client {
uint32_t client_id;
uint32_t risk_profile_id;
// ... 其他客户信息
};
struct Order {
uint64_t order_id;
uint32_t client_id;
uint32_t quantity;
double price;
// ... 其他订单信息
};
// 系统中存储的数据
std::unordered_map<uint32_t, RiskProfile> risk_profiles;
std::unordered_map<uint32_t, Client> clients;
std::unordered_map<uint64_t, Order> orders;
void check_risk_normalized(uint64_t order_id) {
// 第一次查找: 根据 order_id 找到 Order 对象
const auto& order = orders.at(order_id); // 可能的 Cache Miss #1
// 第二次查找: 根据 order.client_id 找到 Client 对象
const auto& client = clients.at(order.client_id); // 可能的 Cache Miss #2
// 第三次查找: 根据 client.risk_profile_id 找到 RiskProfile 对象
const auto& risk =
risk_profiles.at(client.risk_profile_id); // 可能的 Cache Miss #3
// 执行检查
if (order.quantity > risk.max_order_size) {
// ... 拒绝订单 ...
}
}
// --- 去规范化设计 (Denormalized Design for Order Processing) ---
struct EnrichedOrder {
// Order 自身信息
uint64_t order_id;
uint32_t quantity;
double price;
// 冗余存储的 Client 和 Risk 信息
uint32_t client_id;
uint32_t max_order_size;
double max_position_value;
// ... 其他所有在处理该订单时会用到的数据 ...
};
// 系统中只需要一个主要的数据结构
std::unordered_map<uint64_t, EnrichedOrder> enriched_orders;
void check_risk_denormalized(uint64_t order_id) {
// 唯一的一次查找
const auto& order = enriched_orders.at(order_id); // 可能的 Cache Miss #1
// 所有需要的数据都已在同一个对象中,紧密排列在内存里
// 接下来的访问极大概率是 L1 缓存命中 (纳秒级)
if (order.quantity > order.max_order_size) {
// ... 拒绝订单 ...
}
}
3.考虑访问频率与局部性的内存布局
// 内存布局优化
struct alignas(64) OptimalOrder {
// 按访问频率排序字段
uint64_t price; // 最常访问
uint32_t quantity;
uint32_t orderId;
uint64_t timestamp; // 较少访问
char symbol[8]; // 最少访问
// 总计32字节,恰好半缓存行
};
4.struct中的字符数组和字符串指针
#include <stdio.h>
#include <string.h>
#define NAME_LENGTH 32
// 方式1:使用字符数组
typedef struct First {
int nIndex;
char Name[NAME_LENGTH]; // 固定大小的字符数组
} POO_DATA_S;
// 方式2:使用字符串指针
typedef struct Second {
int nIndex;
const char* Name; // 指向常量字符串的指针
} GOO_DATA_S;
// 方式1(字符数组)First:
// - 优点:结构体包含实际的字符数据
// -
// 缺点:占用固定内存空间,即使字符串很短也占用全部NAME_LENGTH空间;无法共享相同字符串,会导致内存里的数据特别松散,访问不紧凑的数据会浪费缓存。
// - 适用场景:字符串长度固定且较短,需要频繁修改字符串内容
// 方式2(字符指针)Second:
// -
// 优点:内存效率高,只存储指针大小;可以指向任何长度的字符串;相同字符串可以共享内存
// - 缺点:需要额外的内存分配和管理;访问需要一次指针跳转
// - 适用场景:存储常量数据,字符串长度变化大,需要节省内存
5.将相关函数分组
类成员函数和非类成员函数通常按它们创建的顺序分配内存地址,因此将频繁调用彼此或操作相同数据集的性能关键函数分组在一起有助于提高代码和数据缓存性能。
当编写性能关键函数时,尽可能将它们放在它们被使用的同一模块中。
尽量依据调用链依次放置函数位置
6.适配缓存组织
- 缓存通常组织成行和集(sets and ways)
- 关键概念:关键步长(critical stride)= (缓存总大小) / (方式数)
- 关键步长表示如果变量在内存中的距离是关键步长的倍数,它们会争夺相同的缓存行,导致缓存争用
在矩阵操作中,如果矩阵的行大小是关键步长的倍数,会导致严重的缓存争用。例如:
- L1缓存关键步长为4KB
- 如果矩阵每行有1024个int(每个int 4字节),则每行大小为4096字节 = 4KB
- 这会导致相邻行的元素争夺同一缓存行,产生缓存争用
为避免这个问题,使用方块分块(tiling)技术,将大矩阵分成小块处理,还可以将矩阵每行大小调整为4097个int(添加一个填充元素),使行大小不为4KB的倍数。
7. 变量:存储与程序内存布局的对应考虑
| 存储类型 | 特点 |
|---|---|
| 栈存储 | 高效(存储局部变量,复用内存区域,对L1缓存友好) |
| 全局/静态存储 | 位于静态区,占用的内存不会主动释放,缓存利用率差 |
| 寄存器存储 | 由编译器选择常用变量存储(一般是局部变量),访问最快 |
| volatile 修饰 | 多用于多线程共享场景;禁止编译器优化(强制直接读写内存) |
| 线程局部存储 | 因为底层访问设计多一次指针解引用效率较栈存储低,优先使用线程栈进行存储 |
| 堆存储(new/delete) | 易产生内存碎片;操作开销大;存在内存泄漏的风险,可能会遭受较差的缓存性能 |
| 类内变量 | 非静态成员按声明顺序连续存储;静态成员存储在静态区 |
8.注意内存硬件的channel和rank参数保证内存负载均衡
CPU内存控制器
│
├── 通道0(Channel 0)
│ ├── DIMM插槽0 : Rank 0, Rank 1
│ └── DIMM插槽1 : Rank 0, Rank 1
│
├── 通道1(Channel 1)
│ ├── DIMM插槽0 : Rank 0, Rank 1
│ └── DIMM插槽1 : Rank 0, Rank 1
│
└── 通道2(Channel 2)
├── DIMM插槽0 : Rank 0, Rank 1
└── DIMM插槽1 : Rank 0, Rank 1
最优填充量 P = (S % (N × 64) == 0) ? 1 : (N × 64) - (S % (N × 64))
其中:
- N = 内存通道数(2、3、4...)
- S = 原始对象大小(字节)
- P = 需要添加的填充字节数
- 64 = 标准缓存行大小(字节)
3通道天然负载均衡,对齐至缓存行大小即可。