我在某处读到了在页面边界旁边执行未对齐的加载或存储之前(例如使用_mm_loadu_si128
/ _mm_storeu_si128
内在函数),代码应首先检查整个向量(在本例中为16个字节)是否属于同一页面,如果没有,则切换到非向量指令。我知道如果下一页不属于进程,则需要这样做以防止coredump。
但是,如果两个页面都属于进程(例如,它们是一个缓冲区的一部分,并且我知道该缓冲区的大小),该怎么办?我写了一个小的测试程序,它执行了未对齐的加载和跨越页面边界的存储,并没有崩溃。在这种情况下,我是否必须始终检查页面边界,还是足以确保我不会溢出缓冲区?
环境:Linux,x86_64,gcc
答案 0 :(得分:9)
页面分割对性能不利,但不会影响未对齐访问的正确性。 当您提前了解长度时,确保您不会读取缓冲区的末尾就足够了。
为了正确起见,在实现诸如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代码wiki中的其他链接,请参阅x86。