我一直以来最受欢迎的博客文章之一是“数据对齐提升速度:神话还是现实?”根据我的仪表盘显示,每周仍有数百人加载这篇旧博文。每年我都会收到几次有人发来的电子邮件,表示不同意。
这篇博文提出了一个简单的观点。程序员经常被告知要担心“未对齐的加载”以提高性能。而我这篇博文的重点是,在优化代码时,通常不必担心对齐问题。
当处理器尝试从内存中读取未正确对齐地址的数据时,就会发生未对齐加载。大多数计算机架构要求访问数据的地址必须是数据大小的倍数(例如,4 字节数据应在能被 4 整除的地址访问)。例如,一个 4 字节的浮点型整数应该从 0x1000 或 0x1004(对齐)的地址加载,但如果尝试从 0x1001(不能被 4 整除)加载,则会发生未对齐加载。在某些情况下,未对齐加载可能会导致系统崩溃,并且通常会导致 C++ 或 C 语言中的“未定义行为”。
与此对齐问题相关的是,数据通常组织在缓存行(64 字节或 128 字节,或大多数系统)中,并一起加载。如果从内存中随机加载数据,可能会触及两条缓存行,从而导致额外的缓存未命中。如果需要加载跨越两条缓存行的数据,则可能会产生性能损失(例如一个周期),因为处理器需要访问这两条缓存行并重新组合数据。此外,还有内存页(4 kB 或更大)的概念。访问额外的页面可能代价高昂,通常希望避免访问不必要的页面。但是,一次加载操作频繁跨越两页,这确实有点不走运。
加载数据时怎么会不对齐呢?这种情况通常发生在访问低级数据结构并将数据赋值给某些字节时。例如,你可能从磁盘加载一个二进制文件,文件可能会认为第一个字节之后的所有字节都是 32 位整数。如果不复制数据,对齐数据可能会很困难。你也可能正在打包数据:假设你有一对值,一个值占首字节,另一个值占 4 个字节。你可以使用 5 个字节而不是 8 个字节来打包这两个值。
有些情况下你需要担心对齐问题。如果你正在编写自己的内存复制函数,你需要符合标准(在 C/C++ 中)或者需要原子操作(用于多线程操作)。
不过,我的总体观点是,这不太可能成为一个性能问题。
鉴于自 2012 年以来我再也没有研究过这个问题,我决定进行一次新的测试。当时我使用了哈希函数。我使用基于 SIMD 的点积,并结合 ARM NEON 内部函数或 AVX2 内部函数。我构建了两个大型 32 位浮点数组,并计算标量积。也就是说,我将元素相乘,然后将乘积相加。这些数组的大小刚好能容纳在一兆字节内,因此我们不会受到 RAM 的限制。
我在 Apple M4 处理器和 Intel Ice Lake 处理器上运行基准测试。
在 Apple M4 上…我们看不到对齐。
字节偏移量 | ns/浮点 | ins/float | 指令/周期 |
---|---|---|---|
0 | 0.18 | 2.00 | 2.40 |
1 | 0.18 | 2.00 | 2.40 |
2 | 0.18 | 2.00 | 2.40 |
3 | 0.18 | 2.00 | 2.40 |
4 | 0.18 | 2.00 | 2.40 |
5 | 0.18 | 2.00 | 2.40 |
6 | 0.18 | 2.00 | 2.40 |
7 | 0.18 | 2.00 | 2.40 |
我们也不能在英特尔 Ice Lake 处理器上做到这一点。
字节偏移量 | ns/浮点 | ins/float | 指令/周期 |
---|---|---|---|
0 | 0.16 | 0.75 | 1.50 |
1 | 0.16 | 0.75 | 1.48 |
2 | 0.16 | 0.75 | 1.49 |
3 | 0.16 | 0.75 | 1.49 |
4 | 0.16 | 0.75 | 1.50 |
5 | 0.16 | 0.75 | 1.50 |
6 | 0.16 | 0.75 | 1.47 |
7 | 0.16 | 0.75 | 1.49 |
我的意思并不是说在某些测试中你无法检测到由于对齐而导致的性能差异。我的意思是,就性能而言,这根本不是你通常应该担心的事情。
原文: https://lemire.me/blog/2025/07/14/dot-product-on-misaligned-data/