Самый быстрый способ работы с невыровненными данными на процессоре с выравниванием по словам?

Я делаю проект на 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 рабочим каждый байт по отдельности действительно самым быстрым решением для таких случаев? Есть ли у кого-нибудь опыт с подобными сценариями, и чувствуете, что разделяете некоторые из их волшебства здесь?

Ответы

Ответ 1

UPDATE

Хорошо, потому что ваши данные не имеют никакого выравнивания, вам нужно либо прочитать все данные в байтах по байт, в правильно выровненные буферы, а затем сделать действительно быстрые 64-битные сравнения или, если вы не будете использовать данные после сравнения, просто считываются в данных в виде байтов и 6 сравниваются, и в этом случае вызов memcmp() может быть лучшим вариантом.


Для не менее 16 бит:


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

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

 return 1;

Будет в два раза быстрее, чем сравнение с байтами, и может быть лучшим, что вы можете разумно сделать до тех пор, пока ваши данные не будут выровнены по крайней мере на 2 байта. Я также ожидал, что компилятор полностью удалит цикл for и просто использует условно выполненные операторы if.

Попытка выполнить 32-битные выровненные чтения не будет быстрее, если вы не сможете гарантировать, что исходный код 1 и 2 аналогично выровнены (add1 и 0x03) == (addr2 и 0x03). Если это так, вы можете прочитать 32-битное значение, а затем 16-битное (или наоборот, в зависимости от начального выравнивания) и удалить еще 1 сравнение.

Поскольку 16-битная ваша общая база, вы можете начать там, и компилятор должен генерировать хорошие коды кода типа ldrh.

Ответ 2

Вы можете заставить свой компилятор выбрать самый быстрый способ для вас:

#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 байтов может быть segfault, если вы не выровнены и запустили страницу, но это начало. Если вы можете добавить два байта заполнения в конец массива, вы можете легко избежать этой проблемы.
gcc и clang, похоже, оптимизируют это.

Ответ 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)));

Page 16

Если вам нужен массив размера, который определяется во время выполнения, то вы будет иметь проблемы с выравниванием. Каждый вектор должен храниться в адрес, делящийся на 16, 32 или 64 байта, в зависимости от его размера. компилятор может это сделать при определении массива фиксированного размера, как в приведенном выше например, но не обязательно с динамическим распределением памяти. Если вы создать массив динамических размеров с помощью новых, malloc или STL контейнер или любой другой метод, то вы не можете получить выравнивание для векторов, и программа, скорее всего, сбой, когда доступ к смещенному вектору. В стандарте С++ говорится: "Это реализация определена, если новое выражение, [...] поддерживает чрезмерно выровненный типы". Возможные решения: использовать posix_memalign, _aligned_malloc, std:: aligned_storage, std:: align и т.д. в зависимости от того, что поддерживается вашим компилятором, но этот метод не может быть переносимым для всех платформ.