影响甚至可以包括正确性,而不仅仅是性能:C 未定义行为 (UB) 导致可能的段错误或其他不当行为,例如,如果您有short
不满足的对象alignof(short)
。 (加载/存储指令默认需要对齐的 ISA 上预计会出现故障,例如 SPARC 和 MIPS64r6 之前的 MIPS。并且即使在 x86 上也可能在编译器优化循环之后,尽管 x86 asm 允许不对齐加载/存储,但某些 16 字节或更宽的 SIMD 除外。)
或撕裂原子操作,如果_Atomic int
没有alignof(_Atomic int)
.
(通常alignof(T) = sizeof(T)
在任何给定的 ABI 中,达到一定的尺寸,通常是寄存器宽度或更宽)。
malloc
应该返回内存alignof(max_align_t)因为您没有任何有关如何使用分配的类型信息。
对于小于的分配sizeof(max_align_t)
, you can如果您愿意,可以返回仅自然对齐的内存(例如,按 4 字节对齐的 4 字节分配),因为您知道存储不能用于具有更高对齐要求的任何内容。
过度对齐的东西,例如动态分配的等价物alignas (16) int32_t foo
需要使用像C11这样的特殊分配器aligned_alloc
。如果您正在实现自己的分配器库,您可能希望支持aligned_realloc 和aligned_calloc,以填补ISO C 无缘无故留下的空白。
并确保你don't实现 Braindead ISO C++17 要求aligned_alloc
如果分配大小不是对齐的倍数,则会失败。没有人想要一个分配器拒绝从 16 字节边界开始的 101 个浮点数的分配,或者更大的浮点数以获得更好的透明大页。aligned_alloc函数要求 and 如何解决 AVX 加载/存储操作的 32 字节对齐问题?
我想我明白 64 位处理器读取 64 位乘 64 位内存
没有。数据总线宽度和突发大小以及加载/存储执行单元最大宽度或实际使用的宽度不必与整数寄存器的宽度相同,也不必与 CPU 定义其位数相同。 (现代高性能 CPU 通常不是这样。例如,32 位 P5 Pentium 有 64 位总线;现代 32 位 ARM 具有执行原子 64 位访问的加载/存储对指令。)
处理器将整个缓存行从 DRAM / L3 / L2 缓存读取到 L1d 缓存;现代 x86 上为 64 字节;在某些其他系统上为 32 字节。
当读取单个对象或数组元素时,它们从 L1d 缓存中读取元素宽度。例如Auint16_t
数组可能仅受益于 2 字节加载/存储的 2 字节边界对齐。
或者,如果编译器使用 SIMD 对循环进行矢量化,则uint16_t
数组可读取 16 或 32bytes一次,即 8 或 16 个元素的 SIMD 向量。 (甚至 AVX512 为 64)。将数组与预期的向量宽度对齐会很有帮助;当不跨越缓存线边界时,未对齐的 SIMD 加载/存储在现代 x86 上运行得很快。
缓存行分割,尤其是页面分割是现代 x86 因未对齐而减慢速度的地方;高速缓存行内未对齐通常不是因为它们将晶体管用于快速未对齐加载/存储。其他一些 ISA 会因任何未对齐而变慢,有些甚至会出现故障,即使在高速缓存行内也是如此。解决方案是相同的:给类型自然对齐:alignof(T) = sizeof(T)。
在您的结构示例中,现代 x86 CPU 不会受到任何惩罚,即使short
未对准。alignof(int) = 4
在任何正常的 ABI 中,所以整个结构有alignof(struct) = 4
, 所以char;short;char
块从 4 字节边界开始。就这样short
包含在单个 4 字节双字内,不跨越任何更宽的边界。 AMD 和英特尔都能够高效地处理这个问题。 (x86 ISA 保证在与 P5 Pentium 或更高版本兼容的 CPU 上对它的访问是原子的,甚至是未缓存的:为什么自然对齐变量的整数赋值在 x86 上是原子的?)
一些非 x86 CPU 会因未对齐短路而受到惩罚,或者必须使用其他指令。 (由于您知道相对于对齐的 32 位块的对齐方式,因此对于加载,您可能会执行 32 位加载和移位。)
所以是的,访问包含以下内容的单个单词是没有问题的short
, but 问题是加载端口硬件提取和零扩展(或符号扩展)short
进入完整的寄存器。这就是 x86 花费晶体管来提高速度的地方。 (@Eric的回答在这个问题的先前版本中,详细介绍了所需的转换。)
将未对齐的存储提交回缓存也很重要。例如,L1d 缓存可能在 32 位或 64 位块(我将其称为“缓存字”)中具有 ECC(针对位翻转的纠错)。因此,仅写入缓存字的一部分以及将其转移到要访问的缓存字内的任意字节边界都是一个问题。 (存储缓冲区中相邻窄存储的合并可以产生全角提交,从而避免在以这种方式处理窄存储的缓存中更新部分字的 RMW 周期)。请注意,我现在说“单词”是因为我谈论的是更面向单词的硬件,而不是像现代 x86 那样围绕未对齐的加载/存储进行设计。See 是否有任何现代 CPU 的缓存字节存储实际上比字存储慢?(存储单个字节仅比未对齐的字节简单一点short
)
(If the short
跨越两个缓存字,当然需要单独的 RMW 周期,每个字节一个。)
当然还有short
错位的原因很简单alignof(short) = 2
并且它违反了此 ABI 规则(假设 ABI 确实具有该规则)。因此,如果您将指向它的指针传递给其他函数,您可能会遇到麻烦。特别是在负载未对齐时出现故障的 CPU 上,而不是在运行时发现负载未对齐时由硬件处理这种情况。然后你可以得到像这样的案例为什么对 mmap 内存的未对齐访问有时会在 AMD64 上出现段错误?其中 GCC 自动向量化预计通过执行 2 字节元素标量的某些倍数来达到 16 字节边界,因此违反 ABI 会导致 x86 上的段错误(通常可以容忍未对齐)。
有关内存访问的完整详细信息,从 DRAM RAS / CAS 延迟到缓存带宽和对齐,请参阅每个程序员都应该了解哪些关于内存的知识?它几乎仍然相关/适用
Also 内存对齐的目的有一个很好的答案。 SO 中还有很多其他很好的答案内存对齐 tag.
有关(某种程度上)现代英特尔加载/存储执行单元的更详细信息,请参阅:https:// electronics.stackexchange.com/questions/329789/how-can-cache-be-that-fast/329955#329955
处理器在读取 64 位时如何知道前 8 位对应于 char,然后接下来的 16 位对应于短整型等等...?
事实并非如此,除了它正在运行以这种方式处理数据的指令之外。
在 asm / 机器代码中,一切都只是字节。每一条指令指定确切地说如何处理哪些数据。由编译器(或人类程序员)在原始字节数组(主内存)之上实现具有类型的变量和 C 程序的逻辑。
我的意思是,在 asm 中,您可以运行任何您想要的加载或存储指令,并且您可以在正确的地址上使用正确的指令。你could加载与两个相邻重叠的 4 个字节int
变量存入浮点寄存器,然后运行addss
(单精度 FP 添加)就可以了,CPU 不会抱怨。但您可能不想这样做,因为让 CPU 将这 4 个字节解释为 IEEE754 二进制 32 浮点不太可能有意义。