【问题标题】:SSE: unaligned load and store that crosses page boundarySSE:跨页面边界的未对齐加载和存储
【发布时间】:2016-10-10 17:28:57
【问题描述】:

我在某处读到,在页面边界旁边执行未对齐加载或存储之前(例如使用_mm_loadu_si128 / _mm_storeu_si128 内在函数),代码应首先检查整个向量(在本例中为 16 个字节)是否属于同一页面,如果没有,则切换到非向量指令。我知道如果下一页不属于进程,则需要这样做以防止核心转储。

但是如果两个页面都属于进程(例如,它们是一个缓冲区的一部分,并且我知道该缓冲区的大小)怎么办?我编写了一个小测试程序,它执行未对齐的加载和存储,它跨越了页面边界,它没有崩溃。在这种情况下我是否必须始终检查页面边界,或者确保我不会溢出缓冲区?

环境:Linux、x86_64、gcc

【问题讨论】:

    标签: c linux x86-64 sse memory-alignment


    【解决方案1】:

    页行拆分不利于性能,但不会影响未对齐访问的正确性。 当您提前知道长度时,确保不会超过缓冲区的末尾就足够了。


    为了正确起见,在实现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 中的其他链接。

    【讨论】:

    • @ZheyuanLi:是的,我很好奇是什么设计改变促成了这一点。 Skylake 还可以并行执行两个页面遍历来解决两个 TLB 未命中问题。这两个事实可能是相关的。
    • 谢谢!。我也没有意识到跨页面访问可能有这么高的成本。所以这绝对是值得寻找的东西。
    • 顺便说一句,Valgrind 有选项 --partial-loads-ok=yes 可以隐藏当加载的数据超过缓冲区末尾时由向量加载引起的“无效读取”问题。
    • @DanielFrużyński - 此选项仅在有限的情况下有效。它不仅允许所有部分加载,而且会努力跟踪加载字中的有效和无效字节,如果随后访问无效字节,仍然会发出“无效读取”警告。不幸的是,许多常见的习惯用法“访问”无效字节,但仍然是正确的,因为它们会丢弃任何此类无效结果 - 例如,memchr 可能会将结果限制为区域的大小。此外,某些其他指令(例如 bsf)也会导致所有字节都被标记为已访问,即使行为正确。
    猜你喜欢
    • 1970-01-01
    • 2017-04-16
    • 2013-12-23
    • 1970-01-01
    • 1970-01-01
    • 2015-06-10
    • 1970-01-01
    • 2018-01-24
    • 2013-11-12
    相关资源
    最近更新 更多