我有一个函数,它本质上在任意内存区域上产生一个哈希值。输入参数使用const空*
类型,作为说“这可以是任何东西”的一种方式。所以本质上:
unsigned hash(const void* ptr, size_t size);
到目前为止,一切都很好。
字节的blob可以是任何东西,它的起始地址可以是任何地方。这意味着有时,它在32位边界上对齐,有时不是。
在某些平台(例如armv6
或mips
)上,从未对齐的内存中读取会导致巨大的性能损失。实际上不可能直接从未对齐的内存中读取32位,因此编译器倾向于满足于更安全的逐字节重组算法(确切的实现细节隐藏在Memcpy()
后面)。
当然,安全访问方法比直接32位访问慢得多,只有当输入数据在32位边界上正确对齐时,才有可能实现这种访问。这导致了一种试图区分两种情况的设计:当输入未对齐时,使用安全访问代码路径;当输入对齐时(实际上非常频繁),使用直接32位访问代码路径。
性能差异很大,我们在这里不是在谈论几个%,这转化为 5 倍的性能提升,有时甚至更多。因此,它不仅仅是“好”,它实际上使功能具有竞争力或否,有用与否。
到目前为止,这种设计在相当多的场景中运行良好。
输入内联。
现在,由于函数实现可以在编译时访问,聪明的编译器可以剥离所有间接层,并将实现简化为其基本元素。在可以证明输入必须对齐的情况下,例如具有已
定义成员的结构,它可以简化代码,删除所有 const void
* 间接寻址,并进入准系统实现,其中使用 const u32*
指针有效读取内存区域。
现在,由于输入区域是使用< code>struct S* ptr编写的,而使用不同的< code>const u32* ptr读取的,因此,允许编译器将这两个操作视为完全独立的,最终对它们进行重新排序,从而导致不正确的结果。
这本质上是我从用户那里得到的解释。值得注意的是,我无法重现该问题,但是通过内联发现了严格的别名问题,这是一个已知的主题。众所周知,由于微小的实现细节导致不同的优化选择,严格的混叠可能难以重现。因此,我认为该报告是可信的,但在没有复制案例的情况下无法直接研究它。
不管怎样,现在问题来了。如何正确处理这种情况?一个“安全”的解决方案是始终使用< code>memcpy()路径,但是它对性能的影响太大,以至于该函数不再有用。另外,这显然是对能源的极大浪费。最简单的方法是不要内联,尽管这会导致它自己的函数调用开销(公平地说,并不是很大),更重要的是只是“隐藏”了问题,而不是解决它。
但是我还没有找到解决它的方法,有人告诉我,无论使用哪种中间指针,即使const char*
是强制转换链的一部分,这也不会阻止最终的const u32*
读取操作违反严格的混淆现象(只是重复,我无法测试它,因为我无法重现案例)。这样描述,这感觉几乎没有希望。
但我不得不注意到,memcpy()
可以适当地避免这种重新排序的风险,尽管它的接口也使用了const void*
,具体的实现方式有很多不同,但我们可以肯定的是,它不仅仅是逐字节读取const char*
,因为性能非常好,而且在更快的情况下使用矢量代码也毫不犹豫。此外,memcpy()
是一个内联的函数。所以我想这个问题一定有解决办法。
(无符号
)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
的值将取决于机器的字节序。如果您需要跨平台一致的哈希,则需要转换为您的首选字节序。
请注意,如果您强制对齐,即使使用 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;
}
问题内容: 我正在开发一个宠物的开源项目,该项目实现了一些流密码算法,并且只有在ARM处理器上运行该bug时,我才遇到问题。我什至尝试在qemu下的x86中运行ARM二进制文件,但该错误并未在那里触发。 该错误的具体机制仍然难以捉摸,但是我最好的选择是相信它是由程序中未对齐的内存访问尝试引起的,这是qemu实现的,但被开发板中的真正ARM处理器默默忽略了。 因此,由于该问题很难诊断,所以我想知道是
问题内容: 在linux系统中,pthreads库为我们提供了用于对齐缓存的功能(posix_memalign),以防止错误共享。要选择架构的特定NUMA节点,我们可以使用libnuma库。我想要的是同时需要两者的东西。我将某些线程绑定到某些处理器,并且我想为来自相应NUMA节点的每个线程分配本地数据结构,以减少线程的内存操作延迟。我怎样才能做到这一点? 问题答案: 如果您只是希望围绕NUMA分配
如果我将循环的迭代次数减少到小于14次,它将不再分段故障。如果我从循环中打印数组索引,它也不再分段错误。 为什么在能够访问未对齐地址的CPU上,未对齐内存访问会出现分段故障,为什么只有在这种特定的情况下才会出现这种情况?
我在用Linux。 我有一个函数叫like: 其功能是: 但我无法访问在线内存: 声明audioExtension: 所以,我希望有: 发生什么事了? 注意:我已尝试: 但无论如何都没用。 musicFile不是常量。我不想声明一个tempchar[80]以避免文件名太长时溢出,如示例cc引用 提前道谢。
当目标指令集为x86/x64时,未对齐的内存读写不会导致错误的结果;而在Emscripten环境下,编译目标为asm.js与WebAssembly时,情况又各有不同。 info 这里“未对齐”的含义是:欲访问的内存地址不是欲访问的数据类型大小的整数倍。 4.2.1 asm.js C代码如下: //unaligned.cc struct ST { uint8_t c[4]; float f; }
问题内容: 对齐是的,没关系,但是对齐的呢?是否保留对齐方式或如何确保重新分配的内存具有相同的对齐方式?假设Linux和x86_64。 问题答案: 不,ISO或POSIX不能保证返回的内存保持相同的对齐方式。A 可以 简单地将当前块扩展到相同的地址,但也可以将其移动到对齐方式比原始地址严格的其他地址。 如果您想要相同的对齐方式,则最好分配另一个块并复制数据。 不幸的是,在单一UNIX规范中也没有任