我正在ARM Cortex M0上做一个项目,它不支持未对齐(4bytes)访问,我正在尝试优化未对齐数据的操作速度。
我将蓝牙低功耗访问地址(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循环单独处理每个字节真的是最快的解决方案吗?有没有人有类似场景的经验,并且想在这里分享他们的一些魔法?
答案 0 :(得分:5)
<强>更新强>
好的,因为你的数据没有对齐,你需要将所有数据逐字节地读入正确对齐的缓冲区,然后进行非常快速的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&amp; 0x03)==(addr2&amp; 0x03)。如果是这种情况,那么您可以读取32位值,然后读取16位(反之亦然,取决于开始对齐)并删除1次比较。
由于16位是您的共享基础,您可以从那里开始,编译器应该生成漂亮的ldrh
类型操作码。
答案 1 :(得分:4)
您可以让编译器为您选择最快的方式:
#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似乎很好地优化了这一点。
答案 2 :(得分:-3)
在阅读这个SIMD级文档时,我发现如何通过正确的对齐方式静态和动态地分配变量。 http://www.agner.org/optimize/vectorclass.pdf
Page 101
Windows,写:
__declspec(align(16)) int mydata[1000];
在类Unix系统中,写一下:
int mydata[1000] __attribute__((aligned(16)));
第16页
如果您需要一个在运行时确定的大小的数组,那么您 对齐会有问题。每个向量都需要存储在 根据其大小,可被16,32或64字节整除的地址。该 编译器可以在定义固定大小的数组时执行此操作,如上所述 例如,但不一定是动态内存分配。 如果你 使用new,malloc或STL创建动态大小的数组 容器,或任何其他方法,那么你可能没有得到正确的 矢量对齐和程序很可能会崩溃 访问未对齐的向量。 C ++标准说“它是 如果new-expression,[...]支持过度对齐,则定义实现 类型“。可能的解决方案是使用posix_memalign,_aligned_malloc, std :: aligned_storage,std :: align 等取决于支持的内容 由您的编译器,但该方法可能无法移植到所有平台。