assembly VMOVDQU的内存参数部分超出分配的范围

2vuwiymt  于 2022-12-13  发布在  其他
关注(0)|答案(1)|浏览(182)

使用SIMD指令处理N字节数据时(一次至少阅读16个字节),通常我只是简单地在缓冲区的末尾添加填充,这样我就可以安全地取整要读取的16字节块的数量。但是,这次我需要处理外部代码准备的数据,因此理论上可能发生最后16字节数据向量部分地福尔斯在所分配的存储器范围之外的情况。
例如,假设我存储了22个字节的数据,从1FFF FFE 4开始:

1FFF FFE0: 00 00 00 00 01 02 03 04 05 06 07 08 09 0A 0B 0C
1FFF FFF0: 0D 0E 0F 10 11 12 13 14 15 16 00 00 00 00 00 00

然后,我要处理16 x 16字节以上的数据,从1FFFFFE 4开始,如下所示:

MOV RDX, 1FFFFFE4 
MOV RCX, 2
@MAIN:
  VMOVDQU XMM0, [RDX]
  ... data processing
  ADD RDX, 16
LOOP @MAIN

最后一次迭代将从1FFFFFF 4读取16个字节,而我在那里只有6个有效字节的数据,其余10个字节可能超出了分配的内存范围(特别是20000000的最后4个字节)。
在最后一次读取部分超出分配的内存范围的情况下(虽然不太可能,但也有可能),或者如果VMOVDQU参数的第一个字节有效,上面的代码会因访问冲突而失败吗?有人能在英特尔64 SDK中指出这方面的确切规则吗?
如果它可能失败,除了以更慢但更安全的方式(逐字节而不是16 × 16字节)处理数据末尾之外,还有其他解决方案吗?这是我以前在这种情况下所做的,但这基本上意味着代码加倍(一个SIMD和一个慢速代码用于同一任务),这是额外的工作和潜在的错误。
由于访问冲突不太可能发生,我也在考虑捕获异常,以安全的方式加载数据,然后跳回-这可以保持代码简单,因为算法本身将保持不变,只需要添加一小段代码,以更安全的方式加载数据,只在非常罕见的情况下执行。在代码下面,但我不知道如何在汇编中捕获异常,也不知道时间损失是否小到有意义:

VMOVDQU XMM0, [RDX]
@DATALOADED:  
... data processing
ADD RDX, 16
... the rest of the algorithm

@EXCEPTION:  // jumps here if the VMOVDQU fails with access violation, happens rarely anyway
...load data in XMM0 in a safer way
JMP @DATALOADED

我正在等待任何其他可以保持代码简单的建议。

wbgh16ku

wbgh16ku1#

下面是我处理这个问题的方法,我使用了一个部分重叠的最终迭代(加上一个可选的初始向量循环对齐)。
这种方法的优点是最后几个元素可以在单个额外的循环迭代中处理。
缺点是:

  • 如果整个数组小于16字节,则需要回退
  • 可能会导致读取-修改-写入循环中代价高昂的加载-存储转发暂停。请将其用于a[i] = b[i] + c[i],但不要用于a[i] += b[i]。如果可以使用别名,则很容易修改代码以捕获a == b || a == c情况并使用回退
  • 移植到AVX 2或AVX 512时,可能需要一些特定于硬件的调整。具体而言:最后的迭代应该使用完整的32或64字节向量,还是应该只用于最后的16字节向量?
  • 如果元素在向量寄存器中不是位置不变的,则不适用,例如,如果您执行混洗、变量移位等操作。

我还加入了一个可选的内存位置对齐;这里我选择了输出。我认为这对于AVX不是特别必要,但它使用了相同的技术,如果您适应SSE 2或AVX 512,可能会很方便。
我用C++编写了这篇文章,其中包含英特尔内部函数,但如果您想将汇编器输出应用到ASM中,它的可读性非常强。

#include <immintrin.h>

#include <cstddef>

void vector_add(float* out, std::ptrdiff_t n, const float* left, const float* right)
{
    __m128 left_i, right_i, out_i;
    std::ptrdiff_t i = 0;
    if(n >= 4) {
#     ifdef ALIGN_OUTPUT
        /*
         * Optional: Do one unaligned iteration, then move the counter
         * up to the first 16-byte aligned output element
         */
        left_i = _mm_loadu_ps(left);
        right_i = _mm_loadu_ps(right);
        out_i = _mm_add_ps(left_i, right_i);
        _mm_storeu_ps(out, out_i);
        i = ((reinterpret_cast<std::ptrdiff_t>(out + 4) & ~15)
            - reinterpret_cast<std::ptrdiff_t>(out)) / sizeof(float);
#     endif
        for(; n - i >= 4; i += 4) {
            left_i = _mm_loadu_ps(left + i);
            right_i = _mm_loadu_ps(right + i);
            out_i = _mm_add_ps(left_i, right_i);
#         ifdef ALIGN_OUTPUT
            _mm_store_ps(out + i, out_i);
#         else
            _mm_storeu_ps(out + i, out_i);
#         endif
        }
        if(n - i > 0) {
            /*
             * Since we know we had at least 4 elements, we can just
             * repeat the operation for the last full vector.
             * If we use ALIGN_OUTPUT, have misaligned pointers, and n == 4,
             * then we compute the same 4 elements twice.
             * Probably not worth fixing
             */
            i = n - 4;
            left_i = _mm_loadu_ps(left + i);
            right_i = _mm_loadu_ps(right + i);
            out_i = _mm_add_ps(left_i, right_i);
            _mm_storeu_ps(out + i, out_i);
        }
        return;
    }
    /* Fallback if n <= 3 */
    if(n >= 2) {
        left_i = _mm_loadl_pi(_mm_undefined_ps(), (const __m64*) left);
        right_i = _mm_loadl_pi(_mm_undefined_ps(), (const __m64*) right);
        out_i = _mm_add_ps(left_i, right_i);
        _mm_storel_pi((__m64*) out, out_i);
        i = 2;
    }
    if(n - i >= 1)
        out[i] = left[i] + right[i];
}

相关问题