【发布时间】:2016-10-14 12:29:37
【问题描述】:
如果允许在输入缓冲区末尾读取少量数据,则高性能算法中的许多方法都可以(并且已经)简化。在这里,“少量”通常意味着在结尾之后最多 W - 1 个字节,其中 W 是算法的字节大小(例如,对于以 64 位块处理输入的算法,最多 7 个字节) .
很明显,写入输入缓冲区的末尾通常是不安全的,因为您可能会破坏缓冲区之外的数据1。同样清楚的是,从缓冲区末尾读取到另一个页面可能会触发分段错误/访问冲突,因为下一页可能不可读。
然而,在读取对齐值的特殊情况下,页面错误似乎是不可能的,至少在 x86 上是这样。在该平台上,页面(以及因此的内存保护标志)具有 4K 粒度(更大的页面,例如 2MiB 或 1GiB,是可能的,但这些是 4K 的倍数),因此对齐读取将仅访问同一页面中的字节作为有效缓冲区的一部分。
以下是一些循环的规范示例,该循环对齐其输入并读取缓冲区末尾后最多 7 个字节:
int processBytes(uint8_t *input, size_t size) {
uint64_t *input64 = (uint64_t *)input, end64 = (uint64_t *)(input + size);
int res;
if (size < 8) {
// special case for short inputs that we aren't concerned with here
return shortMethod();
}
// check the first 8 bytes
if ((res = match(*input)) >= 0) {
return input + res;
}
// align pointer to the next 8-byte boundary
input64 = (ptrdiff_t)(input64 + 1) & ~0x7;
for (; input64 < end64; input64++) {
if ((res = match(*input64)) > 0) {
return input + res < input + size ? input + res : -1;
}
}
return -1;
}
内部函数int match(uint64_t bytes) 未显示,但它会查找与特定模式匹配的字节,如果找到则返回该最低位置 (0-7),否则返回 -1。
首先,为简化说明,将大小 2 的 floor((size - 7) / 8) 块进行循环。这个循环最多可以读取缓冲区末尾的 7 个字节(input & 0xF == 1 时出现 7 个字节的情况)。但是,return 调用有一个检查,它会排除任何出现在缓冲区末尾之外的虚假匹配。
实际上,这样的函数在 x86 和 x86-64 上是否安全?
这些类型的重读在高性能代码中很常见。避免这种 overreads 的特殊尾代码也很常见。有时你会看到后一种类型取代了前一种类型以使 valgrind 等工具静音。有时您会看到 建议 进行这样的替换,但该建议被拒绝,理由是习语是安全的并且工具有错误(或只是过于保守)3。
语言律师须知:
绝对不允许从超出其分配大小的指针中读取 在标准中。我很欣赏语言律师的回答,甚至偶尔写信 他们自己,当有人挖出这一章时,我什至会很高兴 和显示上面代码的诗句是 未定义的行为 因此 严格意义上来说并不安全(我将在此处复制详细信息)。最终,这不是什么 我在追实际上,许多涉及指针的常见习语 转换,结构访问,虽然这样的指针等等 技术上未定义,但在高质量和高 性能代码。通常没有替代品,或者替代品 以一半或更低的速度运行。
如果您愿意,可以考虑修改此问题的版本,即:
上面的代码已经编译成 x86/x86-64 程序集,并且用户已经验证它是按照预期的方式编译的(即, 编译器没有使用可证明的部分越界访问 做点什么really clever, 执行编译的程序安全吗?
在这方面,这个问题既是 C 问题,也是 x86 汇编问题。我见过的大多数使用这个技巧的代码都是用 C 编写的,而 C 仍然是高性能库的主要语言,很容易让 asm 等低级内容和
等高级内容黯然失色。至少在 FORTRAN 仍然打球的核心数字利基之外。所以我对问题的 C-compiler-and-below 视图很感兴趣,这就是为什么我没有将其表述为纯 x86 汇编问题。 说了这么多,虽然我对指向 显示这是 UD 的标准,我对任何细节都非常感兴趣 可以使用此特定 UD 生成的实际实现 意外的代码。现在我不认为这可以在没有一些深入的情况下发生 相当深入的跨过程分析,但是 gcc 溢出的东西 也让很多人感到惊讶...
1 即使在看似无害的情况下,例如,在写回相同值的情况下,它也可以break concurrent code。
2 请注意,此重叠工作需要此函数和 match() 函数以特定的幂等方式运行 - 特别是返回值支持重叠检查。因此,“查找第一个字节匹配模式”有效,因为所有 match() 调用仍然是有序的。但是,“计数字节匹配模式”方法将不起作用,因为某些字节可能会被重复计算。顺便说一句:一些函数,例如“返回最小字节”调用即使没有顺序限制也可以工作,但需要检查所有字节。
3 这里值得注意的是,对于 valgrind 的 Memcheck there is a flag、--partial-loads-ok,它们控制着这些读取是否实际上被报告为错误。默认值为yes,表示通常此类加载不会被视为立即错误,但会努力跟踪加载字节的后续使用,其中一些是有效的,而另一些是有效的不是,如果超出范围的字节被使用,则会标记一个错误。在上述示例中,在match() 中访问整个单词的情况下,即使结果最终被丢弃,这种分析也会得出字节被访问的结论。 Valgrind cannot in general 确定是否实际使用了来自部分加载的无效字节(通常检测可能非常困难)。
【问题讨论】:
-
理论上,C 编译器可以实现自己的检查,这些检查比底层硬件的检查更严格。
-
如果您的用户已经验证它是按“预期方式”编译的,预期方式是访问是安全的,那么它就是安全的。不幸的是,如果您的用户没有阅读汇编中间代码,他/她将不会有任何此类保证。不要这样做。 (您可以通过实现自己的内存管理使其安全)
-
这看起来更像是一个答案而不是一个问题:) 至于特殊的尾部代码,通常只有在算法以块的形式进行但没有首先对齐时才会这样做。
-
嗯,总是有
asm()。 :) -
关于您的第一个问题,C 不保证您正在使用的内存模型甚至对应于底层硬件中的任何“边缘情况”(有几个例外情况诸如字长之类的东西,即使这样它也很挣扎)。所以不要在这方面进行。 “语言法律术语”说“未定义”是有充分理由的。关于第二个问题,您需要发布特定的 ASM 才能使问题有意义。
标签: c performance assembly optimization x86