在字对齐处理器上处理未对齐数据的最快方法?

Fastest way to work with unaligned data on a word-aligned processor?

我正在 ARM Cortex M0 上做一个项目,它不支持未对齐(按 4 字节)访问,我正在尝试优化未对齐数据的操作速度。

我将蓝牙低功耗访问地址(48 位)作为 6 字节数组存储在一些充当数据包缓冲区的打包结构中。由于打包,BLE 地址不一定从字对齐地址开始,在优化我对这些地址的访问函数时,我 运行 遇到了一些麻烦。

第一种也是最明显的方法是对数组中的每个字节分别进行 for 循环操作。例如,检查两个地址是否相同可以这样做:

uint8_t ble_adv_addr_is_equal(uint8_t* addr1, uint8_t* addr2)
{
  for (uint32_t i = 0; i < 6; ++i)
  {
    if (addr1[i] != addr2[i])
      return 0;
  }
  return 1;
}

我在我的项目中做了很多比较,我想看看我是否可以从这个函数中挤出更多的速度。我意识到对于对齐的地址,我可以将它们转换为 uint64_t,并与应用的 48 位掩码进行比较,即

((uint64_t)&addr1[0] & 0xFFFFFFFFFFFF) == ((uint64_t)&addr2[0] & 0xFFFFFFFFFFFF)

写入也可以进行类似的操作,并且对于对齐的版本效果很好。但是,由于我的地址并不总是字对齐(甚至是半字对齐),我将不得不做一些额外的技巧来完成这项工作。

首先,我想到了这个编译器宏的未优化噩梦:

#define ADDR_ALIGNED(_addr) (uint64_t)(((*((uint64_t*)(((uint32_t)_addr) & ~0x03)) >> (8*(((uint32_t)_addr) & 0x03))) & 0x000000FFFFFFFF)\
                                    | (((*((uint64_t*)(((uint32_t)_addr+4) & ~0x03))) << (32-8*(((uint32_t)_addr) & 0x03)))) & 0x00FFFF00000000)

它基本上将整个地址从前一个字对齐的内存位置开始,而不管偏移量。 例如:

    0       1       2       3
|-------|-------|-------|-------|
|.......|.......|.......|<ADDR0>|
|<ADDR1>|<ADDR2>|<ADDR3>|<ADDR4>|
|<ADDR5>|.......|.......|.......|

变成

    0       1       2       3
|-------|-------|-------|-------|
|<ADDR0>|<ADDR1>|<ADDR2>|<ADDR3>|
|<ADDR4>|<ADDR5>|.......|.......|
|.......|.......|.......|.......|

而且我可以安全地对两个地址进行 64 位比较,而不管它们的实际对齐方式如何:

ADDR_ALIGNED(addr1) == ADDR_ALIGNED(addr2)

整洁!但是这个操作在使用 ARM-MDK 编译时需要 71 行汇编,而在简单的 for 循环中进行比较时需要 53 行(我将忽略此处分支指令中花费的额外时间),以及 ~30展开时。此外,它不适用于写入,因为对齐仅发生在寄存器中,而不发生在内存中。再次取消对齐将需要类似的操作,整个方法通常看起来很糟糕。

对于这种情况,展开的 for 循环对每个字节单独处理真的是最快的解决方案吗?有没有人有类似情况的经验,想在这里分享他们的一些魔法?

您可能会让编译器为您选择最快的方式:

#include <stdint.h>
#include <stddef.h>
#include <string.h>

uint64_t unalignedload(char const *packed)
{
  uint64_t buffer;
  memcpy(&buffer, packed, 8);
  return buffer;
}

这不是您想要的,因为如果您未对齐并且 运行 离开页面,加载 8 个字节可能会出现段错误,但这只是一个开始。如果你可以在数组的末尾添加两个字节的填充,你就可以轻松避免这个问题。
gcc 和 clang 似乎对此进行了很好的优化。

更新

好的,因为你的数据无论如何都没有对齐,你需要将所有数据逐字节读取到正确对齐的缓冲区中,然后进行真正快速的 64 位比较,或者,如果你不想在比较之后使用数据,只需以字节形式读入数据并进行 6 次比较,在这种情况下调用 memcmp() 可能是更好的选择。


对于至少 16 位对齐:


 u16 *src1 = (u16 *)addr1; 
 u16 *src2 = (u16 *)addr2;

 for (int i = 0; i < 3; ++i)
 {
    if (src1[i] != src2[i])
      return 0;
 }

 return 1;

将比字节比较快两倍,并且可能是您可以合理执行的最佳操作,只要您的数据至少是 2 字节对齐的。我还希望编译器完全删除 for 循环,而只使用条件执行的 if 语句。

尝试 32 位对齐读取不会更快,除非您可以保证 source1 和 2 类似地对齐 (add1 & 0x03) == (addr2 & 0x03)。如果是这种情况,那么您可以读入 32 位值,然后读入 16 位值(或反之亦然,具体取决于起始对齐方式)并再删除 1 个比较。

由于 16 位是您的共享基础,您可以从那里开始,编译器应该生成不错的 ldrh 类型操作码。

在阅读此 SIMD-class 文档时,我发现了如何以适当的对齐方式静态和动态地分配变量。 http://www.agner.org/optimize/vectorclass.pdf

第 101 页

Windows, write:

__declspec(align(16)) int mydata[1000];

In Unix-like systems, write:

int mydata[1000] __attribute__((aligned(16)));

第 16 页

If you need an array of a size that is determined at runtime, then you will have a problem with alignment. Each vector needs to be stored at an address divisible by 16, 32 or 64 bytes, according to its size. The compiler can do this when defining a fixed-size array, as in the above example, but not necessarily with dynamic memory allocation. If you create an array of dynamic size by using new, malloc or an STL container, or any other method, then you may not get the proper alignment for vectors and the program will very likely crash when accessing a misaligned vector. The C++ standard says "It is implementation defined if new-expression, [...] support over-aligned types". Possible solutions are to use posix_memalign, _aligned_malloc, std::aligned_storage, std::align, etc. depending on what is supported by your compiler, but the method may not be portable to all platforms.