c - 提高读取 volatile 存储器的性能

标签 c performance embedded volatile dma

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

内存是两个二维数组:

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() 的范围内是非 volatile 的,但下次我调用 myTask() 时可能会更改,因此我可以选择 20% 的性能改进?

平台 Cortex-M4

最佳答案

没有 volatile 的问题

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

  • myTask() 时,整个数组可能会加载到缓存中被要求
    第一次。数组可能永远留在缓存中,永远不会
    再次从“主”内存更新。这个问题在多核上比较紧迫
    CPU 如果 myTask()例如,绑定(bind)到单个核心。
  • myTask()被内联到父函数中,编译器可能会决定
    将负载提升到循环外,甚至到 DMA 传输的点
    尚未完成。
  • 编译器甚至可以确定没有写入发生memoryBuffer并假设数组元素始终为 0
    (这将再次触发大量优化)。这可能发生,如果
    该程序相当小,所有代码对编译器都是可见的
    一次(或使用 LTO)。
    记住:毕竟编译器对 DMA 一无所知
    外围设备,它正在“出乎意料地疯狂地写入内存”
    (从编译器的角度来看)。

  • 如果编译器是愚蠢的/保守的并且 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 指令处理多个数组元素
    一条指令。

    如果负载发生在,这些优化通常会被阻止
    volatile 内存,因为编译器通常非常保守
    围绕 volatile 内存访问进行加载/存储重新排序。
    同样,编译器供应商之间的行为有所不同(例如 MSVC 与 GCC)。

    可能的解决方案 1:围栏

    因此,您希望使数组非 volatile ,但为编译器/CPU 添加一个提示,说“当您看到这一行(执行此语句)时,刷新缓存并从内存中重新加载数组”。在 C11 中你可以插入一个 atomic_thread_fence开头myTask() .这样的围栏防止在它们之间重新排序加载/存储。

    由于我们没有 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:操作系统支持

    如果您有(嵌入式)操作系统,您可能会求助于其他模式而不是使用 volatile 数组。我们使用的操作系统具有内存池和队列。后者可以从线程或中断填充,线程可以阻塞
    队列,直到它不为空。该模式看起来有点像这样:
    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);
    }
    

    这可能是最简单的实现方法,但前提是您有操作系统
    如果便携性不是问题。

    关于c - 提高读取 volatile 存储器的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42138283/

    相关文章:

    c - Librsync:Win10和Win8/Win7不同的结果

    javascript - forEach 函数比等效的 for 循环快得多

    c++ - ARM C++ - 如何将 const 成员放入闪存中?

    linux - 串行数据损坏

    c - OpenGL程序不会停止接受输入并且不显示输出

    C - 循环次数减少的冒泡排序

    c - 使用 strtok() 在 c 中将字符串标记两次

    java - 为什么我的 HashMap 实现比 JDK 慢 10 倍?

    windows - 什么可以使程序第二次运行得更快?

    linux - 处理嵌入式板的断电