【问题标题】:Mixing C and assembly and its impact on registers混合 C 和汇编及其对寄存器的影响
【发布时间】:2016-03-11 14:14:07
【问题描述】:

考虑以下将使用 GCC 编译的 C 和 (ARM) 程序集 sn-p:

__asm__ __volatile__ (
        "vldmia.64 %[data_addr]!, {d0-d1}\n\t"
        "vmov.f32 q12, #0.0\n\t"
        : [data_addr] "+r" (data_addr)
        : : "q0", "q12");

for(int n=0; n<10; ++n){
    __asm__ __volatile__ (
            "vadd.f32 q12, q12, q0\n\t"
            "vldmia.64 %[data_addr]!, {d0-d1}\n\t"
            : [data_addr] "+r" (data_addr),
            :: "q0", "q12");
}

在这个例子中,我在循环外初始化一些 SIMD 寄存器,然后让 C 处理循环逻辑,这些初始化的寄存器在循环内使用。

这适用于一些测试代码,但我担心编译器会破坏 sn-ps 之间的寄存器的风险。有什么方法可以确保不会发生这种情况?我可以推断出关于将在 sn-p 中使用的寄存器类型的任何保证(在这种情况下,不会破坏任何 SIMD 寄存器)?

【问题讨论】:

  • 如果代码这么简单,为什么不把它全部变成asm呢?还是只是一个例子?
  • 这是一个简化的示例,但我当然可以将其全部设为 asm。保留只是它使整个代码更难阅读和修改,并且编译器非常擅长输出循环。
  • 无法保证编译器会保留 C 代码中的寄存器。使它成为一个单一语句。并添加一个内存破坏器。
  • 您谨慎考虑这种可能性。最安全的选择是不假设在您的程序集 sn-ps 之间 或周围 使用寄存器。不仅汇编段之间的 C 代码可能会弄乱寄存器,而且您对寄存器的操作也可能会破坏 GCC 的寄存器使用。
  • x86 tag wiki 中有一些关于内联汇编的好东西。例如使用 C 代码从不接触的输出操作数是让 gcc 选择哪些暂存寄存器的好方法,而不是硬编码一些。并且@Olaf:如果代码使用内存操作数而不是在寄存器中请求地址,这可以避免使用内存破坏器。然后编译器会知道哪个内存被触动了。 IIRC,要求使用后递增寻址模式是有限制的。

标签: c gcc assembly


【解决方案1】:

一般来说,在 gcc 中没有办法做到这一点; clobbers 仅保证寄存器将在 asm 调用周围保留。如果您需要确保寄存器保存在两个 asm 部分之间,则需要在第一个部分将它们存储到内存中,然后在第二个部分重新加载。

【讨论】:

  • 显然,当我要编写手工组装时,这不是最理想的!
  • 请注意,编译器可以很好地优化存储和重新加载,因为它具有所有必需的信息。此外,对于 SIMD 的内容,您可能需要查看内置矢量支持。
  • 我有一个想法,我可以使用 SIMD 内在函数将寄存器声明为 C,但某些指令存在问题(无法通过内在函数访问,例如向量乘以标量),需要使用寄存器的子集。
  • 这对于您的目的可能不是最理想的,但不幸的是这是事实。正如 Jester 所说,编译器有可能无论如何都可以对此做些什么。对不起,我不能给你你想要的答案,但在 gcc 或 clang 中没有办法做到这一点。
  • 不是“到内存”,而是“到输出操作数”。如果适合让编译器选择,您可以并且应该使用像"rm" 这样的约束,或者在大多数指令无法使用内存操作数的加载/存储机器上使用"r"。无论如何,这让编译器可以存储它。
【解决方案2】:

编辑:经过多次摆弄后,我得出的结论是,使用下面描述的策略通常比我最初想象的更难解决。

问题在于,特别是当所有寄存器都被使用时,没有什么可以阻止第一个寄存器存储覆盖另一个。我不知道使用可以优化的直接内存写入是否有一些技巧,但初步测试表明编译器可能仍会选择破坏尚未存储的寄存器

目前,在我获得更多信息之前,我将取消将此答案标记为正确,并且在一般情况下,该答案应被视为可能错误。我的结论是,这种对寄存器的本地保护需要在编译器中得到更好的支持才能有用


这绝对可以可靠地做到。利用 @PeterCordes 的 cmets 以及 docs 和一些有用的错误报告(gcc 4153837188),我想出了以下解决方案。

使其有效的关键在于使用临时变量来确保寄存器得到维护(从逻辑上讲,如果循环破坏了它们,那么它们将被重新加载)。在实践中,临时变量已被优化掉,这从检查结果 asm 中可以清楚地看出。

// d0 and d1 map to the first and second values of q0, so we use
// q0 to reduce the number of tmp variables we pass around (instead
// of using one for each of d0 and d1).
register float32x4_t data __asm__ ("q0");
register float32x4_t output __asm__ ("q12");

float32x4_t tmp_data;
float32x4_t tmp_output;

__asm__ __volatile__ (
        "vldmia.64 %[data_addr]!, {d0-d1}\n\t"
        "vmov.f32 %q[output], #0.0\n\t"
        : [data_addr] "+r" (data_addr),
        [output] "=&w" (output),
        "=&w" (data) // we still need to constrain data (q0) as written to.
        ::);

// Stash the register values
tmp_data = data;
tmp_output = output;

for(int n=0; n<10; ++n){

    // Make sure the registers are loaded correctly
    output = tmp_output;
    data = tmp_data;

    __asm__ __volatile__ (
            "vadd.f32 %[output], %[output], q0\n\t"
            "vldmia.64 %[data_addr]!, {d0-d1}\n\t"
            : [data_addr] "+r" (data_addr),
            [output] "+w" (output),
            "+w" (data) // again, data (q0) was written to in the vldmia op.
            ::);

    // Remember to stash the registers again before continuing
    tmp_data = data;
    tmp_output = output;
}

有必要指示编译器q0 被写入每个 asm 输出约束块的最后一行,因此它认为它不能重新排序 data 寄存器的存储和重新加载导致 asm阻止获取无效值。

【讨论】:

  • 你确定你需要tmp的东西吗?
  • 是的,不能保证不会按照文档的规定破坏寄存器。
  • 是的,你似乎是对的。我将删除评论以避免混淆。但也要注意,如果你不特别关心使用q0q12,你可以让编译器选择一个寄存器,这样你就不需要tmp :)
  • 除了q 寄存器似乎没有限制。这个理论就这么多。
  • 非常好。在彼得发表评论之后,我认为类似的事情可能是可能的,但后来被 $realwork 赶上了。你应该接受这个作为答案;我不需要更多的业力来犯错。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-11-11
  • 2021-03-25
  • 2012-01-11
  • 2014-04-25
  • 2011-01-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多