页行拆分不利于性能,但不会影响未对齐访问的正确性。 当您提前知道长度时,确保不会超过缓冲区的末尾就足够了。
为了正确起见,在实现strlen 之类的东西时,您经常需要担心它,当您找到一个标记值时,您的循环就会停止。该值可以位于向量中的任何位置,因此仅执行 16B 未对齐加载将读取数组末尾。如果终止 0 在一个页面的最后一个字节中,并且下一页不可读,并且您的当前位置指针未对齐,则包含 0 字节的加载也将包含来自不可读页面的字节,所以它会出错。
一种解决方案是在指针对齐之前执行标量,然后加载对齐的向量。对齐的负载总是完全来自一个页面,也来自一个缓存行。因此,即使您将读取字符串末尾之后的一些字节,也可以保证不会出错。 Valgrind 可能对此不满意,但标准库 strlen 实现使用它。
您可以从字符串的开头执行未对齐的向量,而不是标量直到对齐的指针(只要它不会跨页行),然后执行对齐的加载。第一个对齐的加载将与第一个未对齐的加载重叠,但对于 strlen 之类的函数来说,这完全没问题,它不关心是否两次看到相同的数据。
出于性能原因,可能值得避免分页。即使您知道您的 src 指针未对齐,让硬件处理缓存行拆分通常会更快。但在 Skylake 之前,页面拆分有额外的 ~100c 延迟。 (Down to 5c in Skylake)。如果您有多个可以相对于彼此以不同方式对齐的指针,则不能总是只使用序言来对齐您的 src。 (例如,c[i] = a[i] + b[i] 和 c 是对齐的,但 b 不是。)
在这种情况下,可能值得使用分支在页面拆分前后进行对齐加载,并将它们与palignr 结合起来。
分支错误预测 (~15c) 比页面拆分延迟更便宜,但会延迟一切(不仅仅是加载)。所以它也可能不值得,这取决于硬件和计算与内存访问的比率。
如果您正在编写一个通常使用对齐指针调用的函数,那么只使用未对齐的加载/存储指令是有意义的。任何检测未对齐的序言对于已经对齐的情况都是额外的开销,并且在现代硬件(Nehalem 和更新的硬件)上,在运行时对齐的地址上的未对齐加载具有与对齐加载指令相同的性能。 (但是您需要 AVX 来将未对齐的加载折叠到其他指令中作为内存操作数。例如 vpxor xmm0, xmm1, [rsi])
通过添加代码来处理未对齐的输入,您可以减慢常见的对齐情况,从而加快不常见的未对齐情况。对未对齐的加载/存储的快速硬件支持让软件只在少数情况下将其留给硬件。
(如果不对齐的输入很常见,那么 值得使用序言来对齐输入指针,尤其是如果您使用的是 AVX。顺序 32B AVX 加载将缓存行拆分每隔一段时间加载一次。)
请参阅Agner Fog's Optimizing Assembly guide 了解更多信息,以及x86 标签 wiki 中的其他链接。