我有一个函数,它主要是在一个任意的内存区域上产生一个哈希值。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()
是一个绝对是内联的函数,所以我想一定有一个解决这个问题的方法。
(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;
}