gcc 内联 asm x86 CPU 标志作为输入依赖项
Posted
技术标签:
【中文标题】gcc 内联 asm x86 CPU 标志作为输入依赖项【英文标题】:gcc inline asm x86 CPU flags as input dependency 【发布时间】: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
并确保您在构建时启用了优化。
【参考方案1】:
我还没有查看 gcc 的 __builtin_add_overflow
输出,但它有多糟糕? @David's 建议使用它,https://gcc.gnu.org/wiki/DontUseInlineAsm 通常很好,特别是如果您担心它会如何优化。 asm
击败了持续传播和其他一些事情。
另外,如果您要使用 ASM,请注意 att 语法是 add %[src], %[dst]
操作数顺序。有关详细信息,请参阅 the tag wiki,除非您总是使用 -masm=intel
构建代码。
有没有办法说“this asm() statement change some cpu flags”和“this asm() use some cpu flags”来表示语句之间的依赖关系并防止重新排序?
没有。 将使用标志的指令 (seto
) 放在与产生标志的指令相同的 asm
块中。 asm
语句可以有许多输入和输出操作数,只受寄存器分配难度的限制(但多个内存输出可以使用具有不同偏移量的相同基址寄存器)。无论如何,包含add
的语句上的额外只写输出不会导致任何低效率。
我打算建议,如果您想从一条指令中输出多个标志,请使用 LAHF 从 FLAGS 加载 AH。但这不包括OF,只包括其他条件代码。这通常很不方便,而且似乎是一个糟糕的设计选择,因为有some unused reserved bits in the low 8 of EFLAGS/RFLAGS,所以 OF 可能与 CF、SF、ZF、PF 和 AF 一起处于低 8 位。但既然不是这样,setc
+ seto
可能比pushf
/ reload 更好,但这值得考虑。
即使有标志输入的语法(就像标志输出的语法一样),让 gcc 插入一些它自己的非标志修改指令也不会非常 (如lea
或mov
)在两个单独的asm
语句之间。
您不希望它们重新排序或任何东西,因此将它们放在同一个 asm 语句中是最有意义的。即使在有序 CPU 上,add
的延迟也很低,因此在其后放置相关指令并不是一个大瓶颈。
顺便说一句,如果溢出是不正常发生的错误情况,jcc
可能会更有效。但不幸的是 GNU C asm goto
不支持输出操作数。您可以获取指针输入并修改内存中的dst
(并使用"memory"
clobber),但强制存储/重新加载比使用setc
或seto
为编译器生成的@ 生成输入更糟糕987654349@/jnz
.
如果您还不需要输出,则可以将 C 标签放在 return true
和 return false
语句上,这(内联后)会将您的代码转换为 jcc 到编译器想要布局的任何位置if()
的分支。例如看看 Linux 是如何做到的:(在这两个示例中我发现了额外的复杂因素):setting up to patch the code 在启动时检查了一次 CPU 功能之后,或者在arch_static_branch
中有一个跳转表部分。)
【讨论】:
以上是关于gcc 内联 asm x86 CPU 标志作为输入依赖项的主要内容,如果未能解决你的问题,请参考以下文章