【问题标题】:How to Optimize Simple Circular/Rotating Buffer/FIFO Handling for Performance如何优化简单的循环/循环缓冲区/FIFO处理以提高性能
【发布时间】:2017-03-07 15:37:24
【问题描述】:

嗨:我一直在学习 C,我有几个基于数组和指针的哲学问题,以及如何让事情变得简单、快速和小,或者至少平衡这三者,我想。

我想象一个 MCU 每隔一段时间对输入进行采样,并将样本存储在一个名为“val”的数组中,大小为“NUM_TAPS”。 'val' 的索引在当前之后的下一个样本中递减,例如,如果 val[0] 刚刚被存储,则下一个值需要进入 val[NUM_TAPS-1]。

最后,我希望能够将最新的样本称为 x[0],将最旧的样本称为 x[NUM_TAPS-1](或等效项)。

这个问题与许多人在这个论坛和其他论坛上解决的描述旋转、循环、队列等缓冲区的问题略有不同。我不需要(我认为)头尾指针,因为我总是有 NUM_TAPS 数据值。我只需要根据“头指针”重新映射索引。

下面是我想出的代码。它似乎运行良好,但它提出了一些我想向更广泛、更专业的社区提出的更多问题:

  • 有没有比条件赋值更好的方法来分配索引 (包装索引 NUM_TAPS -1)?我想不出一种指向指针的方法 帮助,但是其他人对此有什么想法吗?
  • 而不是像在 FIFO 中那样移动数据本身来组织 x 的值,我决定在这里旋转索引。我猜想 对于大小接近或小于指针的数据结构 他们自己认为数据移动可能是要走的路,但对于非常大的 数字(浮点数等)也许指针分配方法是 最有效。想法?
  • 模数运算符通常被认为在速度上接近于 条件语句?例如,通常哪个更快?:

offset = (++offset)%N; *要么** 偏移++; if (NUM_TAPS == offset) { offset = 0; }

谢谢!

#include <stdio.h>

#define NUM_TAPS     10
#define STARTING_VAL  0
#define HALF_PERIOD   3

void main (void) {

  register int sample_offset = 0;
  int wrap_offset = 0;
  int val[NUM_TAPS];
  int * pval;
  int * x[NUM_TAPS];
  int live_sample = 1;

  //START WITH 0 IN EVERY LOCATION
  pval = val; /* 1st address of val[] */
  for (int i = 0; i < NUM_TAPS; i++) { *(pval + i) = STARTING_VAL ; }

  //EVENT LOOP (SAMPLE A SQUARE WAVE EVERY PASS)
  for (int loop = 0; loop < 30; loop++) {
    if (0 == loop%HALF_PERIOD && loop > 0) {live_sample *= -1;}
    *(pval + sample_offset) = live_sample; //really stupid square wave generator

    //assign pointers in 'x' based on the starting offset:
    for (int i = 0; i < NUM_TAPS; i++) { x[i] = pval+(sample_offset + i)%NUM_TAPS; }

    //METHOD #1: dump the samples using pval:
    //for (int i = 0; i < NUM_TAPS; i++) { printf("%3d ",*(pval+(sample_offset + i)%NUM_TAPS)); }
    //printf("\n");

    //METHOD #2: dump the samples using x:
    for (int i = 0; i < NUM_TAPS; i++) { printf("%3d ",*x[i]); }
    printf("\n");

    sample_offset = (sample_offset - 1)%NUM_TAPS; //represents the next location of the sample to be stored, relative to pval
    sample_offset = (sample_offset < 0 ? NUM_TAPS -1 : sample_offset); //wrap around if the sample_offset goes negative
  }
}

【问题讨论】:

    标签: c rotation queue buffer circular-buffer


    【解决方案1】:

    % 运算符的成本约为 26 个时钟周期,因为它是使用 DIV 指令实现的。 if 语句可能更快,因为指令将出现在管道中,因此该过程将跳过一些指令,但它可以快速完成。

    请注意,与只需要 1 个时钟周期的 BITWISE AND 运算相比,这两种解决方案都比较慢。作为参考,如果您想了解详细信息,请查看此图表以了解各种指令成本(以 CPU 时钟滴答数衡量) http://www.agner.org/optimize/instruction_tables.pdf

    对缓冲区索引进行快速取模的最佳方法是使用 2 的幂值作为缓冲区数量,这样您就可以使用快速 BITWISE AND 运算符。

    #define NUM_TAPS     16
    

    缓冲区数量为 2 的幂,您可以使用按位与非常有效地实现模运算。回想一下,按位与 1 使位保持不变,而按位与 0 使位为零。

    因此,假设 NUM_TAPS 为 16,则通过对 NUM_TAPS-1 与递增索引进行按位与运算,它将在值 0,1,2,...,14,15,0,1 之间循环。 .. 这是因为 NUM_TAPS-1 等于 15,即二进制的 00001111b。按位与会产生一个值,其中仅保留最后 4 位,而任何高位都为零。

    因此,无论您在何处使用“% NUM_TAPS”,都可以将其替换为“& (NUM_TAPS-1)”。例如:

    #define NUM_TAPS 16
    ...
    //assign pointers in 'x' based on the starting offset:
    for (int i = 0; i < NUM_TAPS; i++) 
        { x[i] = pval+(sample_offset + i) & (NUM_TAPS-1); }
    

    这是您修改后的代码以使用 BITWISE AND,这是最快的解决方案。

    #include <stdio.h>
    
    #define NUM_TAPS     16  // Use a POWER of 2 for speed, 16=2^4
    #define MOD_MASK     (NUM_TAPS-1) // Saves typing and makes code clearer
    #define STARTING_VAL  0
    #define HALF_PERIOD   3
    
    void main (void) {
    
      register int sample_offset = 0;
      int wrap_offset = 0;
      int val[NUM_TAPS];
      int * pval;
      int * x[NUM_TAPS];
      int live_sample = 1;
    
      //START WITH 0 IN EVERY LOCATION
      pval = val; /* 1st address of val[] */
      for (int i = 0; i < NUM_TAPS; i++) { *(pval + i) = STARTING_VAL ; }
    
      //EVENT LOOP (SAMPLE A SQUARE WAVE EVERY PASS)
      for (int loop = 0; loop < 30; loop++) {
        if (0 == loop%HALF_PERIOD && loop > 0) {live_sample *= -1;}
        *(pval + sample_offset) = live_sample; //really stupid square wave generator
    
        //assign pointers in 'x' based on the starting offset:
        for (int i = 0; i < NUM_TAPS; i++) { x[i] = pval+(sample_offset + i) & MOD_MASK; }
    
        //METHOD #1: dump the samples using pval:
        //for (int i = 0; i < NUM_TAPS; i++) { printf("%3d ",*(pval+(sample_offset + i) & MOD_MASK)); }
        //printf("\n");
    
        //METHOD #2: dump the samples using x:
        for (int i = 0; i < NUM_TAPS; i++) { printf("%3d ",*x[i]); }
        printf("\n");
    
        // sample_offset = (sample_offset - 1)%NUM_TAPS; //represents the next location of the sample to be stored, relative to pval
        // sample_offset = (sample_offset < 0 ? NUM_TAPS -1 : sample_offset); //wrap around if the sample_offset goes negative
    
        // MOD_MASK works faster than the above
        sample_offset = (sample_offset - 1) & MOD_MASK;
      }
    }
    

    【讨论】:

      【解决方案2】:

      最后,我希望能够将最新的样本称为 x[0],将最旧的样本称为 x[NUM_TAPS-1](或等效项)。

      实现这一点的任何方式都非常昂贵,因为每次录制新样本时,都必须移动所有其他样本(或指向它们的指针,或等效项)。指针在这里并不能真正帮助您。事实上,像你这样使用指针可能比直接使用缓冲区更昂贵。

      我的建议是放弃“重新映射”索引的想法持久,而只根据需要虚拟进行。我可能会通过编写数据访问宏来代替直接访问缓冲区来缓解这种情况并确保它始终如一地完成。例如,

      // expands to an expression designating the sample at the specified
      // (virtual) index
      #define SAMPLE(index) (val[((index) + sample_offset) % NUM_TAPS])
      

      然后您将使用SAMPLE(n) 而不是x[n] 来读取样本。

      我可能还会考虑提供一个宏来添加新样本,例如

      // Updates sample_offset and records the given sample at the new offset
      #define RECORD_SAMPLE(sample) do { \
          sample_offset = (sample_offset + NUM_TAPS - 1) % NUM_TAPS; \
          val[sample_offset] = sample; \
      } while (0)
      

      关于您的具体问题:

      • 有没有更好的方法来分配索引,而不是使用模运算符的条件分配(包装索引 NUM_TAPS -1)?我想不出一种指向 指针会有所帮助,但其他人对此有什么想法吗?

      我每次都会选择模数而不是条件。但是,请注意取负数的模数(有关如何避免这样做的示例,请参见上文);这样的计算可能并不意味着你认为它意味着什么。例如-1 % 2 == -1,因为 C 为任何ab 指定(a/b)*b + a%b == a,使得商是可表示的。

      • 我决定在这里轮换索引,而不是像在 FIFO 中那样移动数据本身来组织 x 的值。我猜想 对于大小接近或小于指针的数据结构 他们自己认为数据移动可能是要走的路,但对于非常大的 数字(浮点数等)也许指针分配方法是 最有效。想法?

      但您的实现不会旋转索引。相反,它移动指针。这不仅与转移数据本身一样昂贵,而且还增加了访问数据的间接成本。

      此外,与其他内置数据类型的表示相比,您似乎认为指针表示很小。这种情况很少见。指针通常是给定 C 实现的内置数据类型中最大的。无论如何,围绕数据移动或围绕指针移动都不是有效的。

      • 取模运算符是否通常被认为在速度上接近条件语句?例如,通常哪个更快?:

      在现代机器上,取模运算符的平均速度比 CPU 难以预测的条件运算符快很多。现在的 CPU 有很长的指令流水线,它们会执行分支预测和相应的推测计算,以使它们在遇到条件指令时能够保持这些完整,但是当它们发现自己预测错误时,它们需要刷新整个流水线并重做几个计算。发生这种情况时,它比少量的无条件算术运算要昂贵得多。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-08-13
        • 2022-06-13
        • 2020-08-25
        相关资源
        最近更新 更多