在具有 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

我正在 raspberry pi 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 约束,正如 documentation 所解释的那样,允许编译器将一个字符串插入到 asm 中,该字符串引用寄存器(如 x1)或内存引用([x2][sp, 24] 等),甚至是立即数 (#17)。这对于 CISC 架构来说很好,其中有可以接受上述任何指令的指令(例如 x86 可以执行 add %eax, %ebxadd 24(%rsp), %ebxadd , %ebx),但它对加载存储没有用RISC 体系结构类似于 ARM,因为没有任何指令可以互换使用内存和寄存器。像add, sub, and这样的算术指令只对寄存器进行操作,而load/store指令(ldr / str)只接受内存引用。

如果你要在你的asm中写ldr / str,那么相应的操作数需要是一个内存引用:m约束。

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

所以,更正后的版本应该是这样的

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
    );

顺便说一下,对于仅将输出计算为输入的纯函数且对机器状态没有副作用的代码,不需要 volatile。如果其输出未使用,它会阻止编译器优化 asm 语句。但是在这种情况下,如果您以不再使用 result 的方式更改代码,编译器丢弃死的 asm 将是 good 的事情计算它的代码。


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

扩展 asm 的全部要点是您指定约束以告诉编译器您真正 需要数据的位置,它会相应地安排所有内容。如果您要执行 fadd,您实际上并不需要内存中的数据 - 您需要在寄存器中。所以告诉编译器。

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

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

这样:

  • 代码更短

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

  • 如果值已经在寄存器中,编译器可以只选择它们已经存在的寄存器,避免不必要的 fmovs。如果值在内存中,编译器将生成加载和存储,但仅在需要时才生成。你永远不会有多余的load/store组合

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

参见 the generated asm。请注意,堆栈内存根本不再使用。