【发布时间】:2017-12-29 09:52:05
【问题描述】:
我想创建一个函数,用于添加两个 16 位整数并进行溢出检测。我有用便携式 c 编写的通用变体。但是通用变体对于 x86 目标不是最优的,因为 CPU 在执行 ADD/SUB/etc 时会在内部计算溢出标志。当然,有__builtin_add_overflow(),但在我的情况下,它会生成一些样板文件。
所以我写了以下代码:
#include <cstdint>
struct result_t
{
uint16_t src;
uint16_t dst;
uint8_t of;
};
static void add_u16_with_overflow(result_t& r)
{
char of, cf;
asm (
" addw %[dst], %[src] "
: [dst] "+mr"(r.dst)//, "=@cco"(of), "=@ccc"(cf)
: [src] "imr" (r.src)
: "cc"
);
asm (" seto %0 " : "=rm" (r.of) );
}
uint16_t test_add(uint16_t a, uint16_t b)
{
result_t r;
r.src = a;
r.dst = b;
add_u16_with_overflow(r);
add_u16_with_overflow(r);
return (r.dst + r.of); // use r.dst and r.of for prevent discarding
}
我玩过https://godbolt.org/g/2mLF55 (gcc 7.2 -O2 -std=c++11) 并且结果
test_add(unsigned short, unsigned short):
seto %al
movzbl %al, %eax
addw %si, %di
addw %si, %di
addl %esi, %eax
ret
所以,seto %0 被重新排序。似乎 gcc 认为两个后续的 asm() 语句之间没有依赖关系。并且“cc”clobber 对标志依赖没有任何影响。
我不能使用volatile,因为如果不使用结果(或结果的一部分),seto %0 或整个函数可以(并且必须)优化出来。
我可以为 r.dst 添加依赖项:asm (" seto %0 " : "=rm" (r.of) : "rm"(r.dst) );,并且不会发生重新排序。但这不是“正确的事情”,编译器仍然可以在add 和seto 语句之间插入一些代码更改标志(但不能更改r.dst)。
有没有办法说“this asm() statement change some cpu flags”和“this asm() use some cpu flags”来表示语句之间的依赖关系并防止重新排序?
【问题讨论】:
-
我没有查看您的完整问题,但由于您使用的是 GCC 7.x,您可以使用
=%cc约束来访问特定标志。在你的情况下=%cco。见gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html。否则,您可以将seto放在第一个扩展 asm 语句中,并带有适当的输出约束。 -
@MichaelPetch,例如 GCC 7.x(它是 godbolt.org 的默认值),5.x 和 6.x 也是我的目标。所以我不能使用
=%cco。如果我在第一个asm语句中添加seto,当不使用'overflow'时它不会被丢弃,导致更大的代码和更差的性能(我的目标是最大化性能)。 -
另一个观察结果。在 AT&T 语法中,src 排在第一位,destination 排在第二位(这与 Intel 语法相反)。
-
有人可能会争辩说溢出检查总是合适的。您正在考虑第一个 16 位溢出到第二个。但是第二个 16 位值也可能(可以想象)溢出。不检查明显的错误情况通常是个坏主意。当事情有时崩溃时,你保存的任何性能往往会丢失。此外,您可能希望使用
"qm"而不是rm进行溢出。即使使用 7.x,也无法将标志作为输入约束。我会再次查看__builtin_add_overflow并确保您在构建时启用了优化。
标签: c++ gcc assembly constraints