gcc 内联 ARM 汇编中的 ldm/stm

5
我正在尝试使用内联汇编创建一个ldm(或stm)指令,但在表达操作数时遇到了问题(特别是它们的顺序)。

这是一个简单的示例:
void *ptr;
unsigned int a;
unsigned int b;

__asm__("ldm %0!,{%1,%2}" : "+&r"(ptr), "=r"(a), "=r"(b));

代码无法正常工作,因为它可能会将a放入r1,而将b放入r0

ldm ip!, {r1, r0}

ldm 要求寄存器按升序排列 (因为它们在位域中编码),所以我需要一种方法来表明用于 a 的寄存器低于 b 的寄存器。

一种简单的方法是固定分配寄存器:

register unsigned int a asm("r0");
register unsigned int b asm("r1");

__asm__("ldm %0!,{%1,%2}" : "+&r"(ptr), "=r"(a), "=r"(b));

但这样会削弱很多灵活性,可能会使生成的代码不够优化。
gcc(4.8)是否支持ldm/stm的特殊约束条件?或者,是否有更好的解决方法(例如某些__builtin函数)?
编辑:
由于建议使用“更高级”的构造...我想要解决的问题是将32位字的20位打包(例如,输入为8个字,输出为5个字)。伪代码如下:
asm("ldm  %[in]!,{ %[a],%[b],%[c],%[d] }" ...)
asm("ldm  %[in]!,{ %[e],%[f],%[g],%[h] }" ...) /* splitting of ldm generates better code;
                                                  gcc gets out of registers else */
/* do some arithmetic on a - h */

asm volatile("stm  %[out]!,{ %[a],%[b],%[c],%[d],%[e] }" ...)

这里速度很重要,ldmldr 快50%。由于算法比较棘手,而且 gcc 生成的代码比我好得多 ;) 我想用内联汇编来解决问题,并提供一些关于优化内存访问的提示。


你可能在 b 中指的是 r1。 - auselen
1
你看过这个吗?http://gcc.gnu.org/ml/gcc-help/2007-04/msg00092.html - auselen
@auselen 谢谢你提供的链接,正是我所描述的问题。但是这篇帖子是2007年的,也许现在已经有所改变了吧? - ensc
我觉得不行。“这将需要至少部分重写gcc的寄存器分配器。” http://gcc.gnu.org/ml/gcc-help/2007-04/msg00109.html - auselen
最好的方法是在更高的层次上解决您的问题,并且不受注册机制的限制。 - auselen
1个回答

1
我建议在ARM memtest中采用同样的解决方案,即显式分配寄存器。gcc-help上的分析是错误的。没有必要重新编写GCC的寄存器分配。唯一需要的是允许在汇编规范中对寄存器进行排序。
话虽如此,以下内容将会汇编
int main(void)
{
    void *ptr;
    register unsigned int a __asm__("r1");
    register unsigned int b __asm__("r0");

    __asm__("ldm %0!,{%1,%2}" : "+&r"(ptr), "=r"(a), "=r"(b));
    return 0;
}

这段代码无法编译,因为其中有一条非法的ARM指令ldm r3!,{r1,r0}。解决方法是使用-S标志进行汇编,然后运行一个脚本来排序ldm/stm操作数。这可以通过Perl轻松实现。
$reglist = join(',', sort(split(',', $reglist)));

或者任何其他方式。不幸的是,似乎没有使用汇编约束来实现这一点的方法。如果我们可以访问分配的寄存器编号,则可以使用内联替代或条件编译。
可能最简单的解决方案是使用显式寄存器赋值。除非您正在编写需要加载/存储多个值并且希望给编译器一些生成更好代码的自由的向量库。在这种情况下,最好使用结构,因为较高级别的gcc优化将能够检测到不需要的操作(例如乘以“1”或加上“0”等)。
编辑:
由于有建议使用“更高级别”的结构...我想要解决的问题是将32位字的20位打包(例如,输入是8个字,输出是5个字)。
这可能会产生更好的结果,
  u32 *ip, *op;
  u32 in, out, mask;
  int shift = 0;
  const u32 *op_end = op + 5;

  while(op != op_end) {
     in = *ip++;
     /* mask and accumulate... */
     if(shift >= 32) {
       *op++ = out;
       shift -=32;
     }
  }

ARM流水线通常有几个阶段,其中包括一个单独的加载/存储单元。算术逻辑单元(ALU)可以与加载和存储并行处理。因此,在加载后续字时,您可以同时处理第一个字。在这种情况下,您还可以进行原地替换,这将带来缓存优势,除非您需要重新使用20位值。一旦代码在高速缓存中,如果数据停顿,则ldm/stm的好处很小。这将是您的情况。 第二次编辑:编译器的主要工作不是从内存中加载值,即寄存器分配至关重要。一般来说,ldm/stm 在内存传输函数中最为有用,如内存测试、memcpy() 实现等。如果您正在对数据进行计算,则编译器可能具有更好的管道调度知识。您可能需要接受纯粹的“C”代码或完全转向汇编语言。请记住,ldm 具有立即可用的第一个操作数。使用 ALU 和后续寄存器可能会导致数据加载停顿。同样,stm 需要在执行时完成第一个寄存器计算,但这并不太关键。

顺便提一下,GCC-4.8为IA-32和x86-64重新编写了新的寄存器分配器。实现了新的本地寄存器分配器(LRA),替代了26年前的reload pass,并提高了生成代码质量。目前仅在IA-32和x86-64目标上启用。 - artless noise
在我的情况下,用于生成正确寄存器列表的预处理工作无法实现,因为算术运算对读取数据的顺序非常敏感。 - ensc
在这种情况下,您必须使用“register u32 a asm(”rX“);”,特别是当有8个值时;您最初的问题只有两个。由于有这么多寄存器,您无论如何都会溢出。此外,您最好交错读取/处理/写入。加载/存储是一个单独的单元,因此这可能实际上更快。 “ldm / stm”不会进行流水线处理。 - artless noise
"artless",register只是一个提示关键字,它仅提供类似于inline关键字的建议。除非您已经对其执行操作,否则不能保证您的变量实际上会最终存储在硬件寄存器中。 - sgupta
@ensc,我并不是在说是否需要使用register。我只是在说,按照定义,register关键字仅仅是一个提示。实际上是否满足这个请求取决于编译器,就像inline一样。 - sgupta
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接