提高读取易失性存储器的性能

Den*_*ard 12 c embedded performance volatile dma

我有一个函数从一些易失性存储器读取,由DMA更新.DMA永远不会在与函数相同的内存位置上运行.我的应用程序是性能关键.因此,我意识到执行时间大约提高了.如果我没有将内存声明为volatile,则为20%.在我的函数范围内,内存是非易失性的.Hovever,我必须确保下次调用该函数时,编译器知道内存可能已经改变.

内存是两个二维数组:

volatile uint16_t memoryBuffer[2][10][20] = {0};
Run Code Online (Sandbox Code Playgroud)

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]);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

是否有正确的方法告诉我的编译器memoryBuffer在myTask()范围内是非易失性的,但是下次调用myTask()时可能会更改,所以我可以将性能提高20%?

平台Cortex-M4

Meh*_*olf 6

没有不稳定的问题

我们假设volatile从数据数组中省略了.然后C编译器和CPU不知道它的元素在程序流之外发生了变化.然后可能发生的一些事情:

  • myTask()第一次调用时,整个数组可能会加载到缓存中.该阵列可能永远保留在缓存中,并且永远不会再从"主"内存更新.myTask()例如,如果绑定到单个核心,则此问题在多核CPU上更加紧迫.

  • 如果myTask()内联到父函数中,则编译器可能决定将循环外的负载提升到DMA传输尚未完成的点.

  • 编译器甚至可以确定没有发生写入 memoryBuffer并假设数组元素始终保持为0(这将再次触发大量优化).如果程序相当小并且所有代码一次对编译器可见(或使用LTO),则可能发生这种情况. 记住:毕竟编译器对DMA外设没有任何了解,并且它正在"意外地和疯狂地写入内存"(从编译器的角度来看).

如果编译器是愚蠢/保守的并且CPU不是非常复杂(单核,没有无序执行),那么代码甚至可以在没有volatile声明的情况下工作.但它也可能不会......

易变的问题

制作整个阵列volatile往往是一种悲观.出于速度原因,您可能想要展开循环.因此,而不是从数组加载并交替递增索引,如

load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
load memoryBuffer[m]
m += 1;
Run Code Online (Sandbox Code Playgroud)

一次加载多个元素并以更大的步骤递增索引可以更快

load memoryBuffer[m]
load memoryBuffer[m + 1]
load memoryBuffer[m + 2]
load memoryBuffer[m + 3]
m += 4;
Run Code Online (Sandbox Code Playgroud)

如果负载可以融合在一起(例如,执行一个32位负载而不是两个16位负载),则尤其如此.此外,您希望编译器使用SIMD指令通过单个指令处理多个数组元素.

如果负载发生在易失性存储器中,则通常会阻止这些优化,因为编译器通常非常保守,在易失性存储器访问周围进行加载/存储重新排序.同样,编译器供应商之间的行为也不同(例如MSVC与GCC).

可能的解决方案1:围栏

因此,您希望使数组非易失性,但为编译器/ CPU添加一个提示"当您看到此行(执行此语句)时,刷新缓存并从内存重新加载数组".在C11中,您可以在开头插入atomic_thread_fencemyTask().这样的围栏阻止了它们之间的装载/存储的重新排序.

由于我们没有C11编译器,因此我们使用内在函数来完成此任务.ARMCC编译器具有__dmb()内在(数据存储器屏障).对于GCC,您可能需要查看__sync_synchronize()(doc).

可能的解决方案2:原子变量保持缓冲状态

我们在代码库中使用了以下模式(例如,当通过DMA从SPI读取数据并调用函数来分析它时):缓冲区被声明为普通数组(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) ...
}
Run Code Online (Sandbox Code Playgroud)

将缓冲区与原子配对的优点是,您可以检测何时处理速度太慢意味着您必须缓冲更多,使传入数据更慢或处理代码更快或在您的情况下足够.

可能的解决方案3:OS支持

如果您有(嵌入式)操作系统,则可以使用其他模式而不是使用易失性数组.我们使用的操作系统具有内存池和队列.后者可以从线程或中断填充,并且线程可以阻塞队列,直到它为非空.该模式看起来有点像这样:

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);
}
Run Code Online (Sandbox Code Playgroud)

这可能是最简单的实现方法,但前提是您有操作系统且可移植性不是问题.