如何针对性能优化简单的循环/旋转缓冲区/ FIFO处理

时间:2017-03-07 15:37:24

标签: c rotation queue buffer circular-buffer

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

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

在一天结束时,我希望能够将最新的样本称为x [0],将最旧的样本称为x [NUM_TAPS-1](或等效的)。

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

以下是我提出的代码。它似乎工作正常,但它提出了一些我想向更广泛,更专业的社区提出的问题:

  • 是否有更好的方法来分配索引而不是条件赋值 (使用模数运算符包装索引< 0)(以包装索引> 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
  }
}

2 个答案:

答案 0 :(得分:0)

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

请注意,与执行只需1个时钟周期的BITWISE AND操作相比,这两种解决方案都很慢。作为参考,如果您想要血淋淋的细节,请查看此图表以了解各种指令成本(以CPU时钟滴答为单位) http://www.agner.org/optimize/instruction_tables.pdf

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

#define NUM_TAPS     16

如果缓冲区数量的值为2,则可以使用按位AND来非常有效地实现模数。回想一下使用1的按位AND使该位保持不变,而使用0的按位AND使该位保持为零。

因此,通过使用递增的索引对NUM_TAPS-1进行按位AND,假设NUM_TAPS为16,则它将循环通过值0,1,2,...,14,15,0,1,。 .. 这是有效的,因为NUM_TAPS-1等于15,即二进制的00001111b。按位AND得出的值只保留最后4位,而任何高位都归零。

因此,无论您使用“%NUM_TAPS”,都可以将其替换为“&amp;(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;
  }
}

答案 1 :(得分:0)

  

在一天结束时,我希望能够将最新的样本称为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)

关于您的具体问题:

  
      
  • 使用模数运算符(包装索引&lt; 0)是否有更好的方法来分配索引而不是条件赋值   索引&gt; NUM_TAPS -1)?我想不出指向的方式   指针会有所帮助,但有没有其他人对此有过想法?
  •   

每次我会选择模数超过条件。但是,请注意取负数的模数(参见上面的例子,说明如何避免这样做);这样的计算可能并不意味着你认为它意味着什么。例如-1 % 2 == -1,因为C为任何(a/b)*b + a%b == aa指定b,以便商可以表示。

  
      
  • 我没有像在FIFO中那样移动数据本身来组织x的值,而是决定旋转索引。我猜是的   对于大小接近或小于指针的数据结构   数据移动本身可能是要走的路,但非常大   数字(浮点数等)也许是指针赋值方法   效率最高。想法?
  •   

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

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

  
      
  • 模数运算符通常被认为与条件语句的速度接近吗?例如,哪个通常更快?:
  •   

在现代机器上,模数运算符平均 比条件的结果更快,其结果很难被CPU预测。如今的CPU有很长的指令流水线,它们执行分支预测和相应的推测计算,使它们能够在遇到条件指令时保持这些完整,但是当它们发现它们预测不正确时,它们需要刷新整个流水线并重做几个计算。当发生这种情况时,它比少数无条件的算术运算贵得多。