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

时间:2017-02-09 13:40:54

标签: c performance embedded volatile dma

我有一个函数从一些易失性存储器中读取,该存储器由DMA更新。 DMA永远不会在与函数相同的内存位置上运行。我的应用程序是性能关键。因此,我意识到执行时间大约提高了。如果我没有将内存声明为volatile,则为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%? / p>

Platform Cortex-M4

5 个答案:

答案 0 :(得分:6)

没有易失性的问题

假设数据数组中省略了volatile。然后是C编译器 并且CPU不知道它的元素在程序流之外发生了变化。一些 那些可以发生的事情:

  • 调用myTask()时,整个数组可能会加载到缓存中 第一次。该数组可能永远保留在缓存中,永远不会 从&#34; main&#34;更新记忆了。这个问题在多核问题上更加紧迫 例如,myTask()绑定到单个核心的CPU。

  • 如果将myTask()内联到父函数中,编译器可能会决定 将环路外的负载提升到DMA转移点 尚未完成。

  • 编译器甚至可以确定没有发生写入 memoryBuffer并假设数组元素始终保持为0 (这将再次引发大量优化)。如果这可能发生 该程序相当小,编译器可以看到所有代码 立刻(或使用LTO)。 记住:毕竟编译器对DMA没有任何了解 外围并且它正在意外地和粗暴地写入内存&#34; (从编译器的角度来看)。

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

volatile

的问题

制作 整个数组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:围栏

所以你想让数组非易失性但是为编译器/ CPU添加一个提示&#34;当你看到这一行(执行这个语句)时,刷新缓存并从中重新加载数组存储器&#34; 。在C11中,您可以在myTask()的开头插入atomic_thread_fence。这样的围栏可防止重新排序装载/存储。

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

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

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

这可能是最简单的实施方法,但前提是你有操作系统 如果可移植性不是问题。

答案 1 :(得分:2)

这里你说缓冲区是非易失性的:

  

&#34; memoryBuffer在myTask&#34;范围内是非易失性的。

但在这里你说它必须是不稳定的:

  

&#34;但下次我打电话给myTask时可能会改变#34;

这两句话是矛盾的。显然,内存区域必须易变,或者编译器无法知道它可能会被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
    }
  }
}

您必须对其进行基准测试,但我非常确定这会提高性能。

或者,你可以先复制你感兴趣的数组的整个部分,然后再写回来,然后再写回来。这应该有助于提高绩效。

答案 2 :(得分:1)

你不允许抛弃挥发性限定符 1

如果必须定义包含volatile元素的数组,那么只有两个选项,#34;让编译器知道内存已更改&#34;,是保持volatile限定符,或使用临时数组在没有volatile的情况下定义,并在函数调用后复制到正确的数组。选择哪个更快。

1 (引自:ISO / IEC 9899:201x 6.7.3类型限定符6)
如果是尝试 通过使用左值来引用用volatile限定类型定义的对象 如果使用非volatile限定类型,则行为未定义。

答案 3 :(得分:0)

在我看来,你将缓冲区的一半传递给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]);
    }
  }
}

答案 4 :(得分:0)

我不知道你的平台/ 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;
    }
}