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) );,并且不会发生重新排序。但这不是“正确的事情”,编译器仍然可以在addseto 语句之间插入一些代码更改标志(但不能更改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 插入一些它自己的非标志修改指令也不会非常 (如leamov)在两个单独的asm 语句之间。

您不希望它们重新排序或任何东西,因此将它们放在同一个 asm 语句中是最有意义的。即使在有序 CPU 上,add 的延迟也很低,因此在其后放置相关指令并不是一个大瓶颈。


顺便说一句,如果溢出是不正常发生的错误情况,jcc 可能会更有效。但不幸的是 GNU C asm goto 不支持输出操作数。您可以获取指针输入并修改内存中的dst(并使用"memory" clobber),但强制存储/重新加载比使用setcseto 为编译器生成的@ 生成输入更糟糕987654349@/jnz.

如果您还不需要输出,则可以将 C 标签放在 return truereturn false 语句上,这(内联后)会将您的代码转换为 jcc 到编译器想要布局的任何位置if() 的分支。例如看看 Linux 是如何做到的:(在这两个示例中我发现了额外的复杂因素):setting up to patch the code 在启动时检查了一次 CPU 功能之后,或者在arch_static_branch 中有一个跳转表部分。)

【讨论】:

以上是关于gcc 内联 asm x86 CPU 标志作为输入依赖项的主要内容,如果未能解决你的问题,请参考以下文章

您如何在运行时使用 GCC 和内联 asm 检测 CPU 架构类型?

x86 ASM - cpuid 是不是设置标志?

为内联汇编参数打开立即值传播的特定 GCC 标志是啥?

在 GCC 内联汇编中检索 ZF

GCC 内联汇编中的标签

在 gcc 中使用内联汇编程序调用方法