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

Efficient Embedded Fixed Point 2x2 Matrix Multiplication in ARM Cortex-M4 C code

我正在尝试用 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 数组在内存中应该是连续的,是否有更有效的方式将数据直接传递给函数而无需中间变量?第二个问题,我是不是想太多了,我应该让编译器完成它的工作,因为它比我更聪明?我经常这样做。

谢谢!

您可以打破严格的别名规则,使用 int16_t*int32_t* 类型转换将矩阵行直接加载到 32 位寄存器中。 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 个刻度。

am I overthinking this and I should just let the compiler do its job as it is smarter than I am?

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