繁体   English   中英

在 64 位 Linux 的 Raspberry Pi 4 上的 C 中用汇编语言添加两个双精度浮点数

[英]Adding two double precision floats in assembly language in C on a Raspberry Pi 4 with 64 bit Linux

我正在我的树莓派 4 上学习 ARMV8 汇编语言,我想知道在选择用于存储操作数的寄存器时添加两个浮点数的最简单方法。

我曾希望此代码会将存储在变量 d1 和 d2 中的值相加,然后将总和存储在变量结果中。

#include <stdio.h>
#include <stdlib.h>
int
main()
{
        double d1 = 0.34543;
        double d2 = 1.0;
        double result = 0;
        asm volatile("ldr d1, %1\n\t"
                     "ldr d2, %2\n\t"
                     "fadd d2, d1, d2\n\t"
                     "str d2, %0": "=g" (result) : "g" (d1), "g" (d2)
                    );
        printf("%f + %f = %f", d1, d2, result);
}

相反,当我跑步时

gcc test.c

编译我保存在 test.c 中的上述代码片段,我得到错误:

/tmp/ccdcVUbH.s: Assembler messages:
/tmp/ccdcVUbH.s:31: Error: invalid addressing mode at operand 2 -- `str d2,x0'

当我将代码更改为:

#include <stdio.h>
#include <stdlib.h>
int
main()
{
        double d1 = 0.34543;
        double d2 = 1.0;
        double result = 0;
        printf("%f + %f", d1, d2);
        asm volatile("ldr d1, %1\n\t"
                     "ldr d2, %2\n\t"
                     "fadd d2, d1, d2\n\t"
                     "str d2, %2": "=g" (result) : "g" (d1), "g" (d2)
                    );
        printf(" = %f", d2);
}

我能够编译并运行并得到正确的答案,但令我困扰的是第一个代码片段无法编译,我想知道为什么。

文档所述, g约束允许编译器将一个字符串插入到 asm 中,该字符串引用寄存器(如x1 )或 memory 引用( [x2][sp, 24]等),甚至立即数( #17 )。 这对于 CISC 架构来说很好,其中有可以接受上述任何指令的指令(例如 x86 可以add %eax, %ebxadd 24(%rsp), %ebxadd $17, %ebx ),但它是无用的对于像 ARM 这样的加载存储 RISC 架构,因为没有任何指令可以互换使用 memory 和寄存器。 add, sub, and减法等算术指令仅对寄存器进行操作,加载/存储指令 ( ldr / str ) 仅接受 memory 引用。

如果您要在 asm 中编写ldr / str ,则相应的操作数需要是 memory reference: m约束。

另一个问题是,当您修改 asm 代码中明确选择的寄存器时,您需要通过声明一个clobber来通知编译器。 否则编译器可能会将重要数据保存在该寄存器中而不知道它已被修改。 这可能会导致非常微妙、不可预测和灾难性的错误,这些错误可能只会在优化选项和周围代码的特定组合下出现。 这是内联汇编编程的主要缺陷之一,也是为什么许多人说您根本不应该使用内联汇编,除非有非常充分的理由。

所以,更正后的版本看起来像

asm ("ldr d1, %1\n\t"
     "ldr d2, %2\n\t"
     "fadd d2, d1, d2\n\t"
     "str d2, %0"
     : "=m" (result)
     : "m" (d1), "m" (d2)
     : "d1", "d2" // clobbers
    );

顺便说一句,仅将输出计算为其输入的纯 function 而不会对机器的 state 产生副作用的代码不需要volatile 。如果其输出未使用,它会阻止编译器优化 asm 语句。 但是在这种情况下,如果您以不再使用result的方式更改代码,那么编译器删除计算它的死 asm 代码将是一件好事


现在代码可以正常工作,但仍然效率低下。 您显式地从 memory 加载寄存器,这意味着编译器需要确保这些变量的值实际上memory 中——即使它们之前已经在寄存器中,它最终会在 asm 块之前生成存储指令。 这样您就可以加载以立即返回相同的值:另一端相同,您存储到 memory。编译器必须返回并再次加载。 这是对指令和 memory 带宽的浪费。 请参阅生成的 asm ,第 11-13 行和第 15,17 行。

扩展 asm 的全部要点是您指定约束以告诉编译器您真正需要数据的位置,并且它会相应地安排所有内容。 如果你打算做一个时尚,你并不真的想要fadd中的数据 - 你想要它在寄存器中。 所以告诉编译器。

ARM64 浮点或 SIMD 寄存器的约束是w 但是,默认情况下,这会将寄存器的v名称发送到生成的程序集中: v0, v1等,而您需要d0, d1作为其低 64 位。 您可以使用模板修饰符来解决这个问题。 据我所知,GCC 没有明确记录其对这些的支持,但据我所知,它确实遵循了 armclang 的文档。 d修饰符是我们在这里需要的:

asm ("fadd %d0, %d1, %d2\n\t" 
     : "=w" (result) 
     : "w" (d1), "w" (d2)
    );

这边走:

  • 代码要短得多

  • 您无需手动选择使用哪三个寄存器; 编译器为你选择

  • 如果值已经在寄存器中,编译器可以只选择它们已经存在的寄存器,避免不必要的fmov s。 如果值在 memory 中,编译器将生成加载和存储,但仅在需要时生成。 您永远不会有冗余的加载/存储组合

  • 不需要 clobbers,因为您不修改任何明确命名的寄存器; 只有 output 操作数%d0 ,编译器显然可以告诉你已经修改了它,因为它是一个 output。

查看生成的 asm 请注意,堆栈 memory 根本不再使用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM