16字节NEON操作数的gcc arm内联汇编程序%e0和%f0操作数修饰符?

16字节NEON操作数的gcc arm内联汇编程序%e0和%f0操作数修饰符?,gcc,arm,inline-assembly,neon,Gcc,Arm,Inline Assembly,Neon,找到以下内联汇编程序代码以计算向量叉积: float32x4_t cross_test( const float32x4_t& lhs, const float32x4_t& rhs ) { float32x4_t result; asm volatile( "vext.8 d6, %e2, %f2, #4 \n\t" "vext.8 d7, %e1, %f1, #4 \n\t" "vmul.f32 %e0, %f

找到以下内联汇编程序代码以计算向量叉积:

float32x4_t cross_test( const float32x4_t& lhs, const float32x4_t& rhs )
{
    float32x4_t result;

    asm volatile(
    "vext.8 d6, %e2, %f2, #4 \n\t"          
    "vext.8 d7, %e1, %f1, #4 \n\t"  
    "vmul.f32 %e0, %f1, %e2  \n\t" 
    "vmul.f32 %f0, %e1, d6   \n\t" 
    "vmls.f32 %e0, %f2, %e1  \n\t" 
    "vmls.f32 %f0, %e2, d7   \n\t" 
    "vext.8 %e0, %f0, %e0, #4    "      
    : "+w" ( result )                  
    : "w" ( lhs ), "w" ( rhs )            
    : "d6", "d7" );

    return result;
}
“%”后面的修饰符e和f是什么意思,例如%e2?我找不到这方面的任何参考资料

这是gcc生成的汇编代码:

vext.8 d6, d20, d21, #4 
vext.8 d7, d18, d19, #4 
vmul.f32 d16, d19, d20  
vmul.f32 d17, d18, d6   
vmls.f32 d16, d21, d18  
vmls.f32 d17, d20, d7   
vext.8 d16, d17, d16, #4
我现在明白了所用修饰语的含义。现在我试着遵循叉积算法。为此,我在汇编代码中添加了一些附加注释,但结果不符合我的预期:

    // History:
    // - '%e'  = lower register part
    // - '%f'  = higher register part
    // - '%?0' = res = [ x2 y2 | z2 v2 ]
    // - '%?1' = lhs = [ x0 y0 | z0 v0 ]
    // - '%?2' = rhs = [ x1 y1 | z1 v1 ]
    // - '%e0'       = [ x2 y2 ]
    // - '%f0'       = [ z2 v2 ]
    // - '%e1'       = [ x0 y0 ]
    // - '%f1'       = [ z0 v0 ]
    // - '%e2'       = [ x1 y1 ]
    // - '%f2'       = [ z1 v1 ]
    // Implemented algorithm:
    // |x2|   |y0 * z1 - z0 * y1|
    // |y2| = |z0 * x1 - x0 * z1|
    // |z2|   |x0 * y1 - y0 * x1|
    asm (
    "vext.8 d6, %e2, %f2, #4 \n\t" // e2=[ x1 y1 ], f2=[ z1 v1 ] -> d6=[ v1 x1 ]
    "vext.8 d7, %e1, %f1, #4 \n\t" // e1=[ x0 y0 ], f1=[ z0 v0 ] -> d7=[ v0 x0 ]
    "vmul.f32 %e0, %f1, %e2  \n\t" // f1=[ z0 v0 ], e2=[ x1 y1 ] -> e0=[ z0 * x1, v0 * y1 ]
    "vmul.f32 %f0, %e1, d6   \n\t" // e1=[ x0 y0 ], d6=[ v1 x1 ] -> f0=[ x0 * v1, y0 * x1 ]
    "vmls.f32 %e0, %f2, %e1  \n\t" // f2=[ z1 v1 ], e1=[ x0 y0 ] -> e0=[ z0 * x1 - z1 * x0, v0 * y1 - v1 * y0 ] = [ y2, - ]
    "vmls.f32 %f0, %e2, d7   \n\t" // e2=[ x1 y1 ], d7=[ v0 x0 ] -> f0=[ x0 * v1 - x1 * v0, y0 * x1 - y1 * x0 ] = [  -, - ]
    "vext.8 %e0, %f0, %e0, #4    " // 
    : "+w" ( result )              // Output section: 'w'='VFP floating point register', '+'='read/write'
    : "w" ( lhs ), "w" ( rhs )     // Input section : 'w'='VFP floating point register'
    : "d6", "d7" );                // Temporary 64[bit] register.

首先,这很奇怪。result在asm语句之前未初始化,但它用作带+w result的输入/输出操作数。我认为=w结果会更好。这也是毫无意义的,这是不稳定的;输出是输入的纯函数,没有任何副作用或对任何隐藏输入的依赖,因此相同的输入每次都会给出相同的结果。因此,省略volatile将允许编译器在可能的情况下对其进行CSE并将其从循环中提升出来,而不是在每次源代码使用相同的输入运行它时强制它重新计算

我也找不到任何参考资料;gcc手册的扩展ASM页面仅用于文档,而非ARM

但我认为我们可以从asm输出中看到操作数修饰符的作用:

%e0替换为d16,%f0替换为d17.%e1是d18,%f1是d19.%2在d20和d21中

您的输入是16字节的NEON矢量,在q寄存器中。在ARM32中,每个q寄存器的上半部分和下半部分可以作为d寄存器单独访问。与AArch64不同,AArch64中的每个s/d寄存器是不同q寄存器的底部元素。这段代码似乎是利用这一点,在执行4字节vext shuffle以混合这些浮点对之后,通过在高浮点对和低浮点对上使用64位SIMD来免费洗牌

%e[操作数]是操作数的低位d寄存器,%f[操作数]是高位d寄存器。它们没有文档记录,但gcc源代码在arm_print_操作数中指出:

这两个代码打印Neon quad的低/高双字寄存器 分别登记。对于成对结构类型,也可以打印 低/高四字寄存器

我没有测试如果将这些修饰符应用于64位操作数(如float32x2\u t)会发生什么,这只是一个示例中的反向工程。但这是非常有意义的,会有修饰符

x86修饰符包括一个用于低8位和高8位整数寄存器的修饰符,因此,如果您的输入与EAX中一样,则可以获得AL/AH,因此部分寄存器内容肯定是GNU C内联asm操作数修饰符可以做的事情


请注意,未记录意味着不受支持。

我正在寻找%e0和%f0的含义,这个主题非常有用。交叉测试输出可解释如下:

#include <arm_neon.h>
#include <stdio.h>

float32x4_t cross_test(const float32x4_t& lhs, const float32x4_t& rhs) {
  float32x4_t result;

  //   | f           | e
  // -----------------------------
  // 1 | a3(4) a2(3) | a1(2) a0(1)
  // 2 | b3(5) b2(6) | b1(7) b0(8)
  asm volatile (
    "vext.8 d6, %e1, %f1, #4"  "\n" // a2, a1
    "vext.8 d7, %e2, %f2, #4"  "\n" // b2, b1
    "vmul.f32 %e0, %f1, %e2"   "\n" // a3*b1, a2*b0
    "vmul.f32 %f0, %e1, d7"    "\n" // a1*b2, a0*b1
    "vmls.f32 %e0, %f2, %e1"   "\n" // a3*b1-a1*b3(18), a2*b0-a0*b2(18)
    "vmls.f32 %f0, %e2, d6"    "\n" // a1*b2-a2*b1(-9), a0*b1-a1*b0(-9)
    "vext.8 %e0, %f0, %e0, #4" "\n" // a2*b0-a0*b2(18), a1*b2-a2*b1(-9)
    : "+w"(result) // %0
    : "w"(lhs),    // %1
      "w"(rhs)     // %2
    : "d6", "d7"
  );

  return result;
}

#define nforeach(i, count) \
  for (int i = 0, __count = static_cast<int>(count); i < __count; ++i)

#define dump_f128(qf) do {                    \
  float *fp = reinterpret_cast<float *>(&qf); \
  puts(#qf ":");                              \
  nforeach(i, 4) {                            \
    printf("[%d]%f\n", i, fp[i]);             \
  }                                           \
} while (0)

int main() {
  float fa[] = {1., 2., 3., 4.};
  float fb[] = {8., 7., 6., 5.};

  float32x4_t qa, qb, qres;

  qa = vld1q_f32(const_cast<const float *>(&fa[0]));
  qb = vld1q_f32(const_cast<const float *>(&fb[0]));

  qres = cross_test(qa, qb);

  dump_f128(qa);
  puts("---");
  dump_f128(qb);
  puts("---");

  // -9, 18, -9, -9
  dump_f128(qres);

  return 0;
}

这是内联程序集,不是真正的程序集,不会在arm文档中找到它。剩下的C代码是什么样子的?我添加了完整的C函数。它可以使用gcc进行良好编译。传统FPA寄存器f0-f7。您可以检查反汇编,然后更改一个。检查反汇编,查看结果有何变化。VEXT Vector Extract从第二个操作数向量的底端和第一个操作数向量的顶端提取8位元素,将它们串联起来,并将结果放入目标向量。请参见图6中的示例。仅记录x86的操作数修饰符-即使如此,也不是所有人都赞成这样做,尽管它们是多么重要!。对于那些想使用未记录的aka不支持功能的疯子,请阅读我们看到的:这两个代码分别打印Neon四寄存器的低/高双字寄存器。对于成对结构类型,还可以打印低/高四字寄存器。FWIW@DavidWohlferd:谢谢,我在github上的那个目录里翻了翻,但在modif上搜索什么也找不到。显然e不是一个有用的搜索词。但是,是的,在。你的链接是一个changelog技巧不是搜索e,而是搜索“e”。链接到gcc的SVN。使用下载链接可以从trunk获取最新信息。