ARM Cortex-M4 C 代码中的高效嵌入式定点 2x2 矩阵乘法

问题描述

我正在尝试用 C 代码实现非常高效的 2x2 矩阵乘法,以便在 ARM Cortex-M4 中进行运算。该函数接受 3 个指向 2x2 数组的指针,2 个用于要相乘的输入和一个由 using 函数传递的输出缓冲区。这是我目前所拥有的...

static inline void multiply_2x2_2x2(int16_t a[2][2],int16_t b[2][2],int32_t c[2][2])
{
  int32_t a00a01,a10a11,b00b01,b01b11;

  a00a01 = a[0][0] | a[0][1]<<16;
  b00b10 = b[0][0] | b[1][0]<<16;
  b01b11 = b[0][1] | b[1][1]<<16;
  c[0][0] = __SMUAD(a00a01,b00b10);
  c[0][1] = __SMUAD(a00a01,b01b11);

  a10a11 = a[1][0] | a[1][1]<<16;
  c[1][0] = __SMUAD(a10a11,b00b10);
  c[1][1] = __SMUAD(a10a11,b01b11);
}

基本上,我的策略是使用 ARM Cortex-M4 __SMUAD() 函数来进行实际的乘法累加。但这需要我提前构建输入 a00a01、a10a11、b00b10 和 b01b11。我的问题是,鉴于 C 数组在内存中应该是连续的,是否有更有效的 wat 将数据直接传递给函数而无需中间变量?第二个问题,我是不是想多了,我应该让编译器完成它的工作,因为它比我更聪明?我经常这样做。

谢谢!

解决方法

您可以打破严格的别名规则,将矩阵行直接加载到 32 位寄存器中,使用 int16_t*int32_t* 类型转换。诸如 a00a01 = a[0][0] | a[0][1]<<16 之类的表达式只是从 RAM 中取出一些连续位并将它们排列到寄存器中的其他连续位中。请查阅您的编译器手册以了解该标志以禁用其严格的别名假设,并使转换安全可用。

您也可以通过首先生成转置格式的 b 来避免将矩阵列转置到寄存器中。

了解编译器并了解它在哪些情况下比您更聪明的最佳方法是反汇编其结果并将指令序列与您的意图进行比较。

,

第一个主要问题是 some_signed_int << 16 调用负数的未定义行为。所以你到处都是错误。然后两个 int16_t 的按位或,其中一个为负也不一定形成有效的 int32_t。你真的需要这个标志还是可以放下它?

ARM 示例使用 unsigned int,它依次包含原始二进制形式的 2x int16_t。这也是你真正想要的。

此外,对于 SMUAD 放置哪个 16 位字似乎并不重要。所以 a[0][0] | a[0][1]<<16; 只是在内存中不必要地交换数据。它会混淆无法很好地优化此类代码的编译器。当然,轮班等总是很快,但这是毫无意义的开销。

(正如有人指出的那样,在不考虑所有 C 类型规则和未定义行为的情况下,用纯汇编程序编写整个事情可能要容易得多。)

为了避免所有这些问题,您可以定义自己的联合类型:

typedef union
{
  int16_t  i16 [2][2];
  uint32_t u32 [2];
} mat2x2_t;
  • u32[0] 对应于 i16[0][0]i16[0][1]
  • u32[1] 对应于 i16[1][0]i16[1][1]

C 实际上允许你在这些类型之间“输入双关语”(与 C++ 不同)。联合也避免了脆弱的严格别名规则。

然后该函数可以变成类似于此伪代码的内容:

static uint32_t mat_mul16 (mat2x2_t a,mat2x2_t b)
{
   uint32_t c0 = __SMUAD(a.u32[0],b.u32[0]);
   ...
}

假设每个这样的行应该按照 SMUAD 指令给出 2x 有符号的 16 次乘法。

至于与某些默认 MUL 相比,这是否真的带来了一些革命性的性能提升,我有点怀疑。拆卸并计算 CPU 滴答数。

我是不是想多了,我应该让编译器完成它的工作,因为它比我更聪明吗?

很可能 :) 旧的经验法则:基准测试,然后仅在您实际发现性能瓶颈时手动优化。