【问题标题】:Cache Optimization Theory缓存优化理论
【发布时间】:2014-02-16 12:04:49
【问题描述】:

我正在考虑大量内存缓存优化,并希望得到一些反馈。 考虑这个例子:

class example
    {
        float phase1;
        float phaseInc;
        float factor;
    public:
        void process(float* buffer,unsigned int iSamples)//<-high prio audio thread
        {
            for(unsigned int i = 0; i < iSamples; i++)// mostly iSamples is 32
            {
                phase1 += phaseInc;
                float f1 = sinf(phase1);//<-sinf is just an example!            
                buffer[i] = f1*factor;
            }
        }
    };

优化思路:

 void example::process(float* buffer,unsigned int iSamples)
    {
        float stackMemory[3];// should fit in L1 
        memcpy(stackMemory,&phase1,sizeof(float)*3);// get all memory at once
        for(unsigned int i = 0; i < iSamples; i++)
        {
            stackMemory[0] += stackMemory[1];
            float f1 = sinf(stackMemory[0]);
            buffer[i] = f1*stackMemory[2];
        }
        memcpy(&phase1,stackMemory,sizeof(float)*1);// write back only changed mameory 
    }

请注意,真正的示例循环将包含数千个操作。 所以 stackMemory 可以变得相当大。 我认为它不会超过 32kb(那里有更小的 L1 吗?)。

这个堆栈内存中使用的变量的顺序重要吗? 我希望不会,因为我想订购它们,这样我就可以减少回写大小。 还是 L1 缓存具有与 RAM 相同的缓存线行为?

我感觉我在做预取的用途,但我所读到的关于预取的所有内容都相对模糊,关于如何有效地使用它。尝试和错误不是 5000 多行代码的选项。

代码将在 Win、Mac 和 iOS 上运行。 任何 ARMIntel 问题?

这种优化有没有可能是无用的,因为所有内存都在循环的第一次迭代中被访问并转移到 L1 ?

感谢任何提示和想法。

【问题讨论】:

  • 你是否建议优化3个成员变量的mem访问?无论如何,它们都会被一起获取(并且可能在同一访问中),并在此过程中填充到缓存中。您只是创建了一个额外的副本,这会占用缓存上的额外空间(假设编译器无法消除这种不必要的重复)。
  • 尝试将phase1 += phaseInc; 移出循环(编译器可能已经这样做了)。它变成了phase1 += phaseInc*iSamples;
  • 对我来说这似乎是过早的优化。即使您在循环中调用的唯一函数是sinf,在循环的一次迭代中它也会比其他任何函数都更昂贵。然后你写了“注意,真正的示例循环将包含数千个操作。” 这进一步证实了你正在优化错误的东西。减少循环内的工作并忘记如果你想让你的循环更快,关于缓存。
  • 这个例子应该只是展示我打算如何优化。真正的循环将包含数千行(已经优化的)代码。不幸的是,在音频 DSP 世界中,您有很多数据依赖性,因此不可能将代码从循环中取出。

标签: c++ performance caching optimization


【解决方案1】:

起初我认为第二个很可能会因为memcpy 需要额外的内存访问和指令而变慢,而第一个可以直接使用已经加载到寄存器中的这三个类成员.

尽管如此,我尝试使用 -O2-O3 来修改 GCC 5.2 中的代码,发现无论我尝试什么,我都得到了相同的汇编指令。考虑到 memcpy 通常必须做的所有额外的概念性工作显然被压扁了,这真是太神奇了。

我能想到的一种情况是,在某些情况下,在某些编译器上,您的第二个版本可能会更快,即访问 this-&gt;data_member 所涉及的别名是否干扰了优化并导致对寄存器的冗余加载和存储。

在这种情况下,它与 L1 缓存无关,而与编译器端的寄存器分配有关。当您加载相同的内存(成员变量)时,缓存在很大程度上无关紧要,而不管连续的数据块,它完全与寄存器有关。尽管如此,我找不到一个单一的场景可以导致这种情况发生,即编译器在一个方面做得比另一个方面做得更差——我测试的每个案例都产生了相同的结果。在一个足够复杂的现实世界案例中,也许会有所不同。

再一次,在这种情况下,简单的做法应该更安全:

 void process(float* buffer,unsigned int iSamples)
 {
     const float pi = phaseInc;
     const float p1 = phase1;
     const float fact = factor;
     for(unsigned int i = 0; i < iSamples; i++)
     {
         phase1 += pi;
         float f1 = sinf(p1);
         buffer[i] = f1*fact;
     }
 }

没有必要用memcpy 跳过箍来将结果存储到一个数组中并返回。即使在我的发现中优化器设法消除了通常相关的开销,这也会给优化器带来额外的压力。

我意识到您的示例已简化,但是无论您要处理多少数据成员,都不需要将结构简化为这样的原始数组(除非这样的数组实际上是最方便的表示) .从性能的角度来看,如果您只使用局部变量而不是 memcpy 聚合数据成员进出的数组,编译器将有一个“更轻松”的时间(即使今天的优化器非常出色并且可以处理这个问题)进行优化.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-03
    • 2022-11-23
    • 2013-06-20
    • 2012-06-12
    • 2015-03-05
    • 2011-06-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多