【问题标题】:How can I indicate that the memory *pointed* to by an inline ASM argument may be used?如何指示可以使用内联 ASM 参数*指向*的内存?
【发布时间】:2019-10-19 07:08:49
【问题描述】:

考虑以下小函数:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
}

使用 gcc,this compiles to:

foo:
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

特别注意,第一次写入iptriptr[10] = 1根本不会发生:内联asm nop是函数中的第一件事,只有2的最后一次写入出现(在 ASM 调用之后)。显然编译器决定它只需要提供iptr 的值的最新版本本身,而不是它指向的内存。

我可以用memory clobber 告诉编译器内存必须是最新的,如下所示:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):"memory");
    iptr[10] = 2;
}

这会产生预期的代码:

foo:
        mov     DWORD PTR [rdi+40], 1
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

但是,这是一个太强的条件,因为它告诉编译器所有内存必须被写入。例如,在以下函数中:

void foo2(int* iptr, long* lptr) {
    iptr[10] = 1;
    lptr[20] = 100;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
    lptr[20] = 200;
}

期望的行为是让编译器优化第一次写入lptr[20],而不是第一次写入iptr[10]"memory" clobber 无法实现这一点,因为这意味着两个写入都必须发生:

foo2:
        mov     DWORD PTR [rdi+40], 1
        mov     QWORD PTR [rsi+160], 100 ; lptr[10] written unecessarily
        nop
        mov     DWORD PTR [rdi+40], 2
        mov     QWORD PTR [rsi+160], 200
        ret

有没有办法告诉接受 gcc 扩展 asm 语法的编译器,asm 的输入包括指针和它可以指向的任何东西?

【问题讨论】:

  • @HadiBrais - 是的,但这只是= 的一个副作用,这意味着asm 可能会更改指针值,因此gcc 必须进行两次写入(因为他们可能会写入到不同的位置)。但是,这并不意味着 gcc 必须在调用内联 asm 之前执行 write before(尽管在这种情况下发生了这种情况),因此它通常不起作用(您可以构建一个类似的示例失败的地方)。
  • 另外,你不想"+r"吗?我认为使用"=r" 甚至不需要编译器将指针值传递给asm?
  • 你想要 "具体来说,一个 "m" (*(const float (*)[]) fptr) 会告诉编译器整个数组对象是一个输入,任意长度." 部分来自answer to the linked duplicate
  • 确实如此。但是,如果答案有帮助,那么我想将其标记为重复就可以了。将来可能会发现您的问题的访问者将被指出正确的方向。还是您提出其他解决方案?我们也可以请彼得在这里发布答案,以便他获得学分。
  • @Jester:我认为我们可以使用关于这个主题的独立规范问答。我一直计划在某个时候写一个简单的例子来显示不需要的死存储消除。将它放入我的循环数组答案只是权宜之计。我们想要一个问题,其标题概括了问题,问题正文演示了问题,答案显示了当前最佳实践的虚拟内存输入或输出操作数,并使用 cast-to-array 语法。这对于通过链接向人们显示问题存在是有好处的。

标签: c gcc assembly clang inline-assembly


【解决方案1】:

没错;要求将指针作为内联 asm 的输入并不暗示指向的内存也是输入或输出或两者兼而有之。使用寄存器输入和寄存器输出,所有 gcc 都知道您的 asm 只是通过屏蔽低位来对齐指针,或者向它添加一个常量。 (在这种情况下,您会希望优化掉一个死存储。)

简单的选项是asm volatile"memory" clobber1

您要求的更窄更具体的方法是使用“虚拟”内存操作数以及寄存器中的指针。您的 asm 模板不引用此操作数(可能在 asm 注释中查看编译器选择的内容除外)。它告诉编译器您实际上读取、写入或读取+写入的内存。

虚拟内存输入:"m" (*(const int (*)[]) iptr)
或输出:"=m" (*(int (*)[]) iptr)。或者当然是"+m",语法相同。

该语法转换为指向数组的指针并取消引用,因此实际输入是 C 数组。 (如果您实际上有一个数组,而不是指针,则不需要任何转换,只需将其作为内存操作数即可。)

如果您使用[] 未指定大小,则告诉 GCC 相对于该指针访问的任何内存都是输入、输出或输入/输出操作数。如果您使用 [10][some_variable],告诉编译器具体的大小。对于运行时变量大小,gcc 实际上错过了 iptr[size+1] 不是输入的一部分的优化。

GCC documents this,因此支持它。我认为如果数组元素类型与指针相同,或者如果它是char,这不是严格别名违规。

(来自 GCC 手册)
字符串内存参数长度未知的 x86 示例。

   asm("repne scasb"
    : "=c" (count), "+D" (p)
    : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));

如果您可以避免在指针输入操作数上使用 early-clobber,则虚拟内存输入操作数通常会选择使用相同寄存器的简单寻址模式。

但是,如果您确实使用 early-clobber 来严格确保 asm 循环的正确性,有时虚拟操作数会使 gcc 在内存操作数的基地址上浪费指令(和额外的寄存器)。检查编译器的 asm 输出


背景:

这是 inline-asm 示例中普遍存在的错误,通常未被检测到,因为 asm 被包装在一个函数中,该函数不会内联到任何诱使编译器重新排序存储以进行死存储消除的调用者。

GNU C 内联 asm 语法是围绕向编译器描述 single 指令而设计的。目的是告诉编译器内存输入或内存输出带有"m""=m" 操作数约束,然后它会选择寻址模式。

在内联汇编中编写整个循环需要注意确保编译器确实知道发生了什么(或asm volatile 加上"memory" clobber),否则在更改周围代码或启用链接时优化时有损坏的风险允许跨文件内联。

另见Looping over arrays with inline assembly 使用asm 语句作为循环body,仍然在C 中执行循环逻辑。使用实际(非虚拟)"m""=m" 操作数,编译器可以通过在它选择的寻址模式中使用位移来展开循环。


脚注 1:"memory" clobber 让编译器将 asm 视为非内联函数调用(它可以读取或写入除 escape analysis 已证明未转义的本地内存之外的任何内存)。转义分析包括 asm 语句本身的输入操作数,还包括任何先前调用可能已将指针存储到其中的任何全局或静态变量。所以通常本地循环计数器不必在带有"memory" clobber 的asm 语句周围溢出/重新加载。

asm volatile 是确保 asm 没有被优化掉的必要条件,即使它的输出操作数未使用(因为您需要未声明的写入内存的副作用发生)。

或者对于只被 asm 读取的内存,如果相同的输入缓冲区包含不同的输入数据,您需要再次运行 asm。如果没有volatile,asm 语句可能是CSEd 在循环之外。 ("memory" clobber 不会让优化器在考虑是否需要运行 asm 语句时将所有内存都视为输入。)

没有输出操作数的asm 隐含为volatile,但最好将其明确化。 (GCC 手册中有一个关于asm volatile 的部分)。

例如asm("... sum an array ..." : "=r"(sum) : "r"(pointer), "r"(end_pointer) : "memory") 有一个输出操作数,所以不是隐式易失的。如果你像这样使用它

 arr[5] = 1;
 total += asm_sum(arr, len);
 memcpy(arr, foo, len);
 total += asm_sum(arr, len);

如果没有volatile,第二个asm_sum 可以优化掉,假设具有相同输入操作数(指针和长度)的相同asm 将产生相同的输出。对于不是其显式输入操作数的纯函数的任何 asm,您都需要 volatile。如果没有优化,那么"memory" clobber 将具有要求内存同步的预期效果。

【讨论】:

  • 等我有时间的时候,我会用一个 Godbolt 的例子来扩展它。
  • 仅供参考:破坏特定数量的字节并不总是像人们希望的那样工作:gcc.gnu.org/bugzilla/show_bug.cgi?id=63900
  • @DavidWohlferd:谢谢,我没有意识到它会影响其他阵列。我认为最坏的情况只是它将运行时变量长度视为无限;我没有用第二个数组测试过。这可能是因为char* 能够为任何东西起别名?我会测试它...
  • 从那里的其他cmet可以看出,我对这里的所有内部机制都没有清楚的了解(BLKmode?)。然而,其他人似乎看到了相同的结果。我的收获很简单,就像大多数欺骗编译器的尝试一样,这个并不总是有效。好吧,实际上它确实“有效”,因为它总是产生正确的答案,但并不总是完全最优的。
  • @PeterCordes 引用 GCC 邮件列表中讨论此功能的开发人员博客或线程就足够了。请注意,relevant code in GCC 并非微不足道,例如只有当 regclass 被认为是“小”(无论特定目标选择什么意思)时,它才会合并寄存器。
猜你喜欢
  • 2011-08-17
  • 1970-01-01
  • 2016-04-03
  • 1970-01-01
  • 2022-09-27
  • 2012-02-11
相关资源
最近更新 更多