【问题标题】:Why this code extremely slow?Any thing related to cache behavior?为什么这段代码非常慢?有什么与缓存行为有关的吗?
【发布时间】:2018-01-22 03:14:03
【问题描述】:

我开始了一些面向数据的设计实验。我最初开始做一些 oop 代码,发现有些代码非常慢,不知道为什么。这是一个例子: 我有一个游戏对象

    class GameObject
    {
    public:
          float m_Pos[2];
          float m_Vel[2];
          float m_Foo;

          void UpdateFoo(float f){
             float mag = sqrtf(m_Vel[0] * m_Vel[0] + m_Vel[1] * m_Vel[1]);
             m_Foo += mag * f;
          }
     };

然后我使用 new 创建 1,000,000 个对象,然后循环调用 UpdateFoo()

        for (unsigned i=0; i<OBJECT_NUM; ++i)
        {
           v_objects[i]->UpdateFoo(10.0);
        }

完成循环大约需要 20 毫秒。而当我注释掉float m_Pos[2]时发生了奇怪的事情,所以对象看起来像这样

    class GameObject
    {
    public:
          //float m_Pos[2];
          float m_Vel[2];
          float m_Foo;

          void UpdateFoo(float f){
             float mag = sqrtf(m_Vel[0] * m_Vel[0] + m_Vel[1] * m_Vel[1]);
             m_Foo += mag * f;
          }
     };

突然循环需要大约 150 毫秒才能完成。如果我在 m_Vel 之前放任何东西,速度会快得多。我尝试在 m_Vel 和 m_Foo 或其他地方之间放置一些填充,除了 m_Vel 之前的地方....慢。

我在发布版本 i7-4790 中测试了 vs2008 和 vs2010 知道这种差异是如何发生的吗?是否与任何缓存一致行为有关。

这是整个样本:

    #include <iostream>
    #include <math.h>
    #include <vector>
    #include <Windows.h>

    using namespace std;

    class GameObject
    {
    public:
        //float m_Pos[2];
        float m_Velocity[2];
        float m_Foo;

        void UpdateFoo(float f)
        {
          float mag = sqrtf(m_Velocity[0] * m_Velocity[0] + m_Velocity[1] * 
                            m_Velocity[1]);
          m_Foo += mag * f;
         }
    };



     #define OBJECT_NUM 1000000

     int main(int argc, char **argv)
     {
       vector<GameObject*> v_objects;
       for (unsigned i=0; i<OBJECT_NUM; ++i)
       {
          GameObject * pObject = new GameObject;
          v_objects.push_back(pObject);
       }

       LARGE_INTEGER nFreq;
       LARGE_INTEGER nBeginTime;
       LARGE_INTEGER nEndTime;
       QueryPerformanceFrequency(&nFreq);
       QueryPerformanceCounter(&nBeginTime);

       for (unsigned i=0; i<OBJECT_NUM; ++i)
       {
           v_objects[i]->UpdateFoo(10.0);
       }

       QueryPerformanceCounter(&nEndTime);
       double dWasteTime = (double)(nEndTime.QuadPart-
                       nBeginTime.QuadPart)/(double)nFreq.QuadPart*1000;

       printf("finished: %f", dWasteTime);

       //   for (unsigned i=0; i<OBJECT_NUM; ++i)
       //   {
       //       delete(v_objects[i]);
       //   }
     }

【问题讨论】:

  • 你能提供一个完整的例子来说明你是如何计时这段代码的吗?
  • 不要指望 MSVC;将其放入gcc.godbolt.org 并进行比较。
  • 您的方法可以使用一些内联。浮点运算通常比双倍慢...
  • 从您提供的有限信息来看,可能性几乎是无穷无尽的。尝试提供一个minimal reproducible example,其中包括您所描述的课程和您的计时方法。
  • 听起来像是不正确的基准测试或试图对未优化的代码进行基准测试。

标签: c++ data-oriented-design


【解决方案1】:

然后我使用 new 创建 1,000,000 个对象,然后循环 调用 UpdateFoo()

你的问题就在那里。不要单独分配一百万个需要使用通用分配器重复处理的小东西。

尝试将对象连续存储或存储在连续的块中。一个简单的解决方案是将它们全部存储在一个大的std::vector 中。要在恒定时间内删除,您可以将要删除的元素与最后一个交换并弹回。如果您需要稳定的索引,您可以留下一个孔以便在插入时回收(可以使用空闲列表或堆栈方法)。如果您需要不会失效的稳定指针,deque 可能是结合使用空闲列表或单独的索引堆栈来回收/覆盖的“漏洞”想法的选项。

您也可以只使用一个空闲列表分配器并对其使用placement new,同时小心地使用相同的分配器释放并手动调用dtor,但这会变得更快,并且比数据结构方法需要更多的练习才能做好。相反,我建议您将游戏对象存储在某个大容器中,以便您重新控制所有内容将驻留在内存中的位置以及由此产生的空间位置。

我在发布版本中测试了 vs2008 和 vs2010,i7-4790 知道如何 这种差异会发生吗?它是否与任何缓存相关 行为。

如果您正确地进行基准测试和构建项目,那么当GameObject 较小时,分配器可能会更多地碎片化内存,从而导致更多的缓存未命中。这似乎是最可能的解释,但如果没有好的分析器,很难确定。

也就是说,我建议使用上述解决方案,而不是进一步分析它,这样您就不必担心分配器将内存中的每一个小东西分配到哪里。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-02
    相关资源
    最近更新 更多