Я делаю проект на ARM Cortex M0, который не поддерживает неглавный (на 4 байта) доступ, и я пытаюсь оптимизировать скорость операций с неизмененными данными.
Я храню адреса Bluetooth с низким энергопотреблением (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)
Ухоженная! Но эта операция занимает 71 строку сборки при компиляции с ARM-MDK, по сравнению с 53 при сравнении в простом цикле (я просто буду игнорировать дополнительное время, потраченное в инструкциях от ветвления) и ~ 30 при разворачивании. Кроме того, он не работает для записи, поскольку выравнивание происходит только в регистрах, а не в памяти. Повторное выравнивание потребует аналогичной операции, и весь подход, как правило, сосут.
Является ли развернутым for-loop рабочим каждый байт по отдельности действительно самым быстрым решением для таких случаев? Есть ли у кого-нибудь опыт с подобными сценариями, и чувствуете, что разделяете некоторые из их волшебства здесь?