Повысить производительность чтения энергозависимой памяти

У меня есть функция чтения из некоторой энергозависимой памяти, которая обновляется DMA. DMA никогда не работает в том же месте памяти, что и функция. Мое приложение критично критично. Следовательно, я понял, что время выполнения улучшено на ок. 20%, если я не объявляю память нестабильной. В рамках моей функции память является энергонезависимой. Hovever, я должен быть уверен, что в следующий раз, когда вызывается функция, компилятор знает, что память может быть изменена.

Память представляет собой два двумерных массива:

volatile uint16_t memoryBuffer[2][10][20] = {0};

DMA работает с противоположной "матрицей", чем программная функция:

void myTask(uint8_t indexOppositeOfDMA)
{
  for(uint8_t n=0; n<10; n++)
  {
    for(uint8_t m=0; m<20; m++)
    {
      //Do some stuff with memory (readings only):
      foo(memoryBuffer[indexOppositeOfDMA][n][m]);
    }
  }
}

Есть ли способ сообщить моему компилятору, что memoryBuffer является энергонезависимым в области myTask(), но может быть изменен в следующий раз, когда я вызову myTask(), чтобы я мог улучшить производительность на 20%?

Платформа Cortex-M4

Ответ 1

Проблема без летучих

Предположим, что volatile не указан в массиве данных. Затем компилятор C и ЦП не знает, что его элементы изменяются вне потока программы. Некоторые вещи, которые могут произойти тогда:

  • Весь массив может быть загружен в кеш при вызове myTask() первый раз. Массив может оставаться в кеше навсегда и никогда снова обновляется с "основной" памяти. Эта проблема более насущна для многоядерных CPU, если myTask() привязан к одному ядру, например.

  • Если myTask() встроен в родительскую функцию, компилятор может решить для поднятия нагрузки вне цикла даже до точки, где передача DMA не завершено.

  • Компилятор может даже определить, что запись не происходит memoryBuffer и предположим, что элементы массива остаются в 0 все время (что снова вызовет множество оптимизаций). Это может произойти, если программа была довольно небольшой, и весь код виден компилятору (или используется LTO). Помните: после того, как компилятор ничего не знает о DMA и что он пишет "неожиданно и дико в память", (с точки зрения компилятора).

Если компилятор тупой/консервативный, а CPU не очень сложный (одноядерный, без выполнения вне порядка), код может работать даже без объявления volatile. Но это также может не...

Проблема с энергозависимой

Making весь массив volatile часто является пессимизацией. По причинам скорости вы вероятно, хотите развернуть цикл. Поэтому вместо загрузки из массив и приращение индекса чередующимся образом, например

load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;

он может быстрее загружать сразу несколько элементов и увеличивать индекс в больших шагах, таких как

load memoryBuffer[m]
load memoryBuffer[m + 1]
load memoryBuffer[m + 2]
load memoryBuffer[m + 3]
m += 4;

Это особенно верно, если нагрузки могут быть сплавлены вместе (например, для выполнения одна 32-разрядная загрузка вместо двух 16-разрядных нагрузок). Далее вы хотите, чтобы компилятор для использования команды SIMD для обработки нескольких элементов массива с помощью одна инструкция.

Эти оптимизации часто предотвращаются, если загрузка происходит из энергозависимой памяти, поскольку компиляторы обычно очень консервативны с загрузить/сохранить переупорядочивание по обращениям с энергозависимой памятью. Снова поведение отличается от поставщиков компиляторов (например, MSVC и GCC).

Возможное решение 1: ограждения

Итак, вы хотите сделать массив нестабильным, но добавить подсказку для компилятора/процессора, говорящего "когда вы видите эту строку (выполнить этот оператор), очистите кеш и перезагрузите массив из памяти". В C11 вы можете вставить atomic_thread_fence в начале myTask(). Такие ограждения препятствуют переупорядочению нагрузок/хранилищ на них.

Поскольку у нас нет компилятора C11, мы используем intrinsics для этой задачи. Компилятор ARMCC имеет __dmb() встроенный (барьер памяти данных). Для GCC вы можете посмотреть __sync_synchronize() (doc).

Возможное решение 2: атомная переменная, содержащая состояние буфера

Мы используем следующий шаблон в нашей кодовой базе (например, при чтении данных из SPI через DMA и вызов функции для ее анализа): буфер объявляется как простой массив (no volatile) и в каждый буфер добавляется атомный флаг, который устанавливается, когда передача DMA завершена. Код выглядит чем-то например:

typedef struct Buffer
{
    uint16_t data[10][20];
    // Flag indicating if the buffer has been filled. Only use atomic instructions on it!
    int filled;
    // C11: atomic_int filled;
    // C++: std::atomic_bool filled{false};
} Buffer_t;

Buffer_t buffers[2];

Buffer_t* volatile currentDmaBuffer; // using volatile here because I'm lazy

void setupDMA(void)
{
    for (int i = 0; i < 2; ++i)
    {
        int bufferFilled;
        // Atomically load the flag.
        bufferFilled = __sync_fetch_and_or(&buffers[i].filled, 0);
        // C11: bufferFilled = atomic_load(&buffers[i].filled);
        // C++: bufferFilled = buffers[i].filled;

        if (!bufferFilled)
        {
            currentDmaBuffer = &buffers[i];
            ... configure DMA to write to buffers[i].data and start it
        }
    }

    // If you end up here, there is no free buffer available because the
    // data processing takes too long.
}

void DMA_done_IRQHandler(void)
{
    // ... stop DMA if needed

    // Atomically set the flag indicating that the buffer has been filled.
    __sync_fetch_and_or(&currentDmaBuffer->filled, 1);
    // C11: atomic_store(&currentDmaBuffer->filled, 1);
    // C++: currentDmaBuffer->filled = true;

    currentDmaBuffer = 0;
    // ... possibly start another DMA transfer ...
}

void myTask(Buffer_t* buffer)
{
    for (uint8_t n=0; n<10; n++)
        for (uint8_t m=0; m<20; m++)
            foo(buffer->data[n][m]);

    // Reset the flag atomically.
    __sync_fetch_and_and(&buffer->filled, 0);
    // C11: atomic_store(&buffer->filled, 0);
    // C++: buffer->filled = false;
}

void waitForData(void)
{
    // ... see setupDma(void) ...
}

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

Возможное решение 3: поддержка ОС

Если у вас есть (встроенная) ОС, вы можете использовать другие шаблоны вместо использования изменчивых массивов. В ОС мы используем пулы памяти и очереди. Последний может быть заполнен из потока или прерывания, и поток может блокироваться очередь, пока она не будет пустой. Шаблон выглядит примерно так:

MemoryPool pool;              // A pool to acquire DMA buffers.
Queue bufferQueue;            // A queue for pointers to buffers filled by the DMA.
void* volatile currentBuffer; // The buffer currently filled by the DMA.

void setupDMA(void)
{
    currentBuffer = MemoryPool_Allocate(&pool, 20 * 10 * sizeof(uint16_t));
    // ... make the DMA write to currentBuffer
}

void DMA_done_IRQHandler(void)
{
    // ... stop DMA if needed

    Queue_Post(&bufferQueue, currentBuffer);
    currentBuffer = 0;
}

void myTask(void)
{
    void* buffer = Queue_Wait(&bufferQueue);
    [... work with buffer ...]
    MemoryPool_Deallocate(&pool, buffer);
}

Это, вероятно, самый простой подход к реализации, но только если у вас есть ОС и если переносимость не является проблемой.

Ответ 2

Здесь вы говорите, что буфер нестабилен:

"memoryBuffer является энергонезависимым в области myTask"

Но здесь вы говорите, что он должен быть неустойчивым:

", но может быть изменен в следующий раз, когда я вызову myTask"

Эти два предложения противоречат друг другу. Очевидно, что область памяти должна быть энергозависимой или компилятор не может знать, что она может быть обновлена ​​DMA.

Однако я скорее подозреваю, что фактическая потеря производительности связана с доступом к этой области памяти неоднократно через ваш алгоритм, заставляя компилятор читать его снова и снова.

Что вы должны сделать, это взять локальную, энергонезависимую копию той части интересующей вас памяти:

void myTask(uint8_t indexOppositeOfDMA)
{
  for(uint8_t n=0; n<10; n++)
  {
    for(uint8_t m=0; m<20; m++)
    {
      volatile uint16_t* data = &memoryBuffer[indexOppositeOfDMA][n][m];
      uint16_t local_copy = *data; // this access is volatile and wont get optimized away

      foo(&local_copy); // optimizations possible here

      // if needed, write back again:
      *data = local_copy; // optional
    }
  }
}

Вам нужно будет сравнить его, но я уверен, что это должно повысить производительность.

В качестве альтернативы вы можете сначала скопировать всю интересующую вас часть массива, а затем поработать над этим, прежде чем записывать ее обратно. Это должно помочь производительности еще больше.

Ответ 3

Вам не разрешено отбрасывать изменчивый классификатор 1.

Если массив должен быть определен с использованием летучих элементов, то только два параметра, "которые позволяют компилятору знать, что память изменилась", должны сохранять изменчивый классификатор или использовать временный массив, который определяется без изменчивости и после вызова функции копируется в соответствующий массив. Выберите то, что быстрее.


1 (Цитируется по: ISO/IEC 9899: 201x 6.7.3 Типовые классификаторы 6)
Если попытка чтобы ссылаться на объект, определенный с помощью нестабильного типа, посредством использования значения lvalue с типом энергонезависимого типа, поведение undefined.

Ответ 4

Мне кажется, что вы получаете половину буфера до myTask, и каждая половина не обязательно должна быть изменчивой. Поэтому я задаюсь вопросом, можете ли вы решить свою проблему, определив буфер как таковой, а затем передав указатель на один из полубуферов на myTask. Я не уверен, будет ли это работать, но может быть что-то вроде этого...

typedef struct memory_buffer {
    uint16_t buffer[10][20];
} memory_buffer ;

volatile memory_buffer double_buffer[2];

void myTask(memory_buffer *mem_buf)
{
  for(uint8_t n=0; n<10; n++)
  {
    for(uint8_t m=0; m<20; m++)
    {
      //Do some stuff with memory:
      foo(mem_buf->buffer[n][m]);
    }
  }
}

Ответ 5

Я не знаю вас, платформа /mCU/SoC, но обычно DMA прерывают этот триггер на программируемом пороге.

Я могу представить, как удалить ключевое слово volatile и использовать прерывание в качестве семафора для задачи.

Другими словами:

  • DMA запрограммирован на прерывание при записи последнего байта буфера
  • Задача - это блок с флагом семафора/флагом, ожидающий освобождения флага
  • Когда DMA вызывает процедуру прерывания, сжимает буфер, указанный DMA для следующего времени чтения, и меняет флаг, который разблокирует задачу, которая может разрабатывать данные.

Что-то вроде:

uint16_t memoryBuffer[2][10][20];

volatile uint8_t PingPong = 0;

void interrupt ( void )
{    
    // Change current DMA pointed buffer

    PingPong ^= 1;    
}

void myTask(void)
{
    static uint8_t lastPingPong = 0;

    if (lastPingPong != PingPong)
    {
        for (uint8_t n = 0; n < 10; n++)
        {
            for (uint8_t m = 0; m < 20; m++)
            {
                //Do some stuff with memory:
                foo(memoryBuffer[PingPong][n][m]);
            }
        }

        lastPingPong = PingPong;
    }
}