使用快速u32访问读取对齐的内存,当与内联搭配使用时,会导致严格的别名问题。

问题描述 投票:1回答:1

我有一个函数,它主要是在一个任意的内存区域上产生一个哈希值。const void* 类型,以此来表达 "这可以是任何东西"。所以基本上......。

unsigned hash(const void* ptr, size_t size);

到目前为止,还不错。

字节块可以是任何东西,它的起始地址可以是任何地方。意思是说,有时,它是在32位边界上对齐的,有时不是。

在某些平台上(armv6 例如,或 mips),从不对齐的内存读取会导致巨大的性能惩罚。实际上不可能直接从不对齐的内存中读取32位,所以编译器倾向于采用更安全的逐字节重组算法(具体实现细节隐藏在一个 memcpy()).

安全访问方法当然是 好多 这就导致了在设计上要区分两种情况:当输入未对齐时,使用安全访问代码路径,当输入对齐时(实际上经常)使用直接的32位访问代码路径。

性能上的差异是巨大的,我们在这里谈论的不是几个%,这相当于5倍的性能提升,有时甚至更多。所以,这不仅仅是 "好看",实际上是让功能变得有竞争力,或者说有用,或者说没有。

这个设计到目前为止,在相当多的场景下,都能正常使用。

进入内联。

现在,在编译时可以访问函数的实现,一个聪明的编译器可以剥离所有的间接层,并将实现还原为其基本元素。在它能证明输入必然是对齐的情况下,比如一个 struct 与定义的成员,它可以简化代码,删除所有的 const void* indirections,然后再到裸体实现,在那里,内存区域被有效地使用一个叫做 const u32* 指针。

现在,严格的锯齿可以开始了,因为输入区域是用一个 struct S* ptr,并使用不同的 const u32* ptr,因此允许编译器将这两个操作视为完全独立的操作,最终将它们重新排序,导致错误的结果。

这基本上是我从一个用户那里得到的解释。可能值得注意的是,我无法重现这个问题,但通过内联发现的严格别名问题,这是一个已知的话题。众所周知,由于微小的实现细节导致不同编译器版本的优化选择不同,严格的aliasing也很难重现。因此,我认为这个报告是可信的,但在没有重现案例的情况下,无法直接研究。

总之,现在问题来了。如何正确的处理这种情况?memcpy() 路径,但它使性能大打折扣,使函数不再有用。另外,这显然是一种可怕的能源浪费。简单的方法是不内联,虽然这导致了自己的函数调用开销(公平的说,没有那么大),更重要的是只是 "隐藏 "了问题,而不是解决问题。

但我还没有找到解决的办法。有人告诉我,无论使用什么样的中间指针,即使是一个 const char* 是铸造链的一部分,但这并不妨碍最终的。const u32* 读操作违反严格的别名(只是重复一下,我无法测试,因为我无法重现这种情况).这样描述,感觉几乎没有希望。

但我不能不注意到 memcpy() 可以适当地避免这种重新排序的风险,尽管它的界面也使用了 const void*,具体的实现方式也有很多不同,但我们可以肯定的是,它不仅仅是逐个字节的读取。const char*,因为性能很好,在速度较快的时候,毫不犹豫地使用矢量代码。另外。memcpy() 是一个绝对是内联的函数,所以我想一定有一个解决这个问题的方法。

c hash inline strict-aliasing memory-access
1个回答
0
投票

(unsigned) char豁免 从严格的别名规则。无论怎样,只要以下内容是安全的、理智的。sizeof(uint32_t) == 4:

unsigned hash(const void* ptr, size_t size) {
    const unsigned char* bytes = ptr;

    while (size >= 4) {
        uint32_t x;
        memcpy(&x, bytes, 4);
        bytes += 4;
        size -= 4;

        // Use x.
    }

    // Size leftover bytes.
}

请注意: x 将取决于您的机器的endianness。如果你需要跨平台一致的哈希,你将需要转换到你喜欢的endianness。


请注意,如果你强制对齐,你可以让编译器生成快速路径代码,即使是在有了 memcpy:

void* align(void* p, size_t n) {
    // n must be power of two.
    uintptr_t pi = (uintptr_t) p;
    return (unsigned char*) ((pi + (n - 1)) & -n);
}

inline uint32_t update_hash(uint32_t h, uint32_t x) {
    h += x;
    return h;
}

unsigned hash(const void* ptr, size_t size) {
    const unsigned char* bytes = (unsigned char*) ptr;
    const unsigned char* aligned_bytes = align((void*) bytes, 4);

    uint32_t h = 0;
    uint32_t x;
    if (bytes == aligned_bytes) {
        // Aligned fast path.
        while (size >= 4) {
            memcpy(&x, bytes, 4);
            h = update_hash(h, x);
            size -= 4;
            bytes += 4;
        }
    } else {
        // Slower unaligned path, copy to aligned buffer.
        while (size >= 4) {
            uint32_t buffer[32];
            size_t bufsize = size < 4*32 ? size / 4 : 32;
            memcpy(buffer, bytes, 4*bufsize);
            size -= 4*bufsize;

            for (int i = 0; i < bufsize; ++i) {
                h = update_hash(h, buffer[i]);
            }
        }
    }

    if (size) {
        // Assuming little endian.
        x = 0;
        memcpy(&x, bytes, size);
        h = update_hash(h, x);
    }

    return h;
}
© www.soinside.com 2019 - 2024. All rights reserved.