面向对象,数据方向,缓存污染和缓存显而易见性

时间:2012-09-11 08:20:10

标签: performance oop caching memory-efficient data-oriented-design

在常规的面向对象实践中,并不是罕见的对象具有多个不相关的成员属性。当物体被处理时,在不同的通道中完成它并不罕见,这些通道针对其属性的不同部分。

在这方面,创建对象集合的典型方法似乎不是非常有效的方法。考虑到计算机访问内存的方式和缓存行的平均大小,很有可能缓存内存正在填充不需要的内容,但恰好相邻,所以最终浪费缓存容量并增加停顿和执行的延迟。

更糟糕的是使用多态和动态分配对象的做法,没有内存池和自定义分配器。在这种情况下,不仅缓存会被不需要的数据填充,而且由于动态内存分配使用的任意地址,预取程序也无法正常工作。

救赎是回到OOP之前的时间并选择数据导向,这似乎是开发性能关键应用程序,操作系统等的首选。但为什么不使用两者的混合版本呢?排序面向数据的对象编程

经过长时间的提议,让我们来看看手头的问题。我没有足够大的项目来测试这个概念的效率,因此非常欢迎社区的理论专业知识。

除了存储自己的数据成员的对象之外,它们只存储对集合的引用,其中数据成员按顺序存储在它们自己的容器中,并且它们的成员方法从这些容器返回数据,这样的几率是应该减少在前往CPU的过程中不需要的数据,并且增加近期“未来”所需的数据几率。逻辑假设是这种方法可以提高预取器效率,缓存命中率和使用效率,还可以减少自动和手动并行化所涉及的延迟。

您怎么看?

延迟编辑:如果我们考虑结构和类填充,如果“模型”具有charint数据成员,那么应用“数据方向模式”会更有益在OOP方式中,它将被填充,这将仅进一步污染缓存,但是面向数据的存储模式可以按顺序存储所有char和所有int,而不会浪费空间和缓存所有

2 个答案:

答案 0 :(得分:0)

首先,好的幻灯片演示文稿。好吧,据我所知,你的问题与演示方法有很大的不同。变量随机存储在主存储器中,甚至是对象属性。如果你试图将内存分配给一个连续的数据结构,你的数据结构的大小将受到主存储器中最大的“气泡”的限制,否则它将不是纯粹的连续性。也许你想到这样的事情:

class MyClass
{
public:
    MyClass()
    {
        m_dataMembers = new GenericObject[DATA_MEMBERS_AMOUNT];

        //initialize array values...
    }

    int getMyVar()
    {
        return (int)m_dataMembers[MY_VAR_INDEX];
    }

    //others functions...

private:
    GenericObject* m_dataMembers;
}

这样,你会遇到一些问题。首先,您需要一个通用对象类来存储任何类型的变量。然后,您需要知道每个变量在数据结构中的位置,然后您需要知道数据结构中每个变量的类型,以便在getter中正确转换。他在演示文稿中实际做的是减少他的类大小,使用引用,使其更适合缓存页面,并减少缓存中的使用,而不是在主内存中。我希望我没有误会你。

答案 1 :(得分:0)

我看到的方式是,如果在非常精细的粒状对象(如抽象IPixel接口)的层次上使用它,则对象级别的多态性本质上是昂贵的。在这种情况下,围绕IPixel依赖关系的视频处理软件从效率的角度来看会非常紧张,因为它没有喘息的优化空间。除了每像素动态调度的成本之外,这里所需的虚拟指针也可能比整个像素本身更大,使用内存增加一倍或三倍。此外,我们不能再以超出单个像素的方式使用像素表示,而且,最恐怖的是,图像中的相邻像素甚至可能在内存中不连续地表示。

同时IImage可以提供足够的优化空间,因为图像模拟像素的集合/容器,并且仍具有足够的灵活性(例如:针对每种像素格式的不同具体图像表示)。现在,每个图像的动态调度很便宜,并且虚拟指针的大小对于整个图像来说可以忽略不计。我们还可以探索如何以一种允许我们一次有效处理多个像素的方式将像素表示到我们心中的内容。所以我认为这与你类似,就像在适当的粗糙度上设计对象一样,这通常意味着事物的集合,以减少所有开销和优化的障碍。

  

而不是存储自己的数据成员的对象,它们   仅存储对其数据成员所在的集合的引用   按顺序存储在自己的容器及其成员中   方法从这些容器返回数据,这种方式的几率   应该减少在前往CPU的路上不需要的数据   数据的可能性,在未来"未来"增加了。

我喜欢这个想法,但是如果你对多态上下文采取了太多的分析,你可以按照自定义内存分配器和排序基本指针的方式工作。我经常发现这种设计的用途是提高使用单个元素的便利性,以便在需要聚合以提高效率的情况下(一个案例是使用SoA表示进入容器,另一个我将在下面介绍) )。

多态情况并不一定能从中受益,因为固有的问题在于一次一个地对粒状物体进行非均匀处理。为了恢复效率,我们必须恢复关键循环的同质性。

非同质关键圈

举例Orc继承CreatureHuman继承CreatureElf继承Elves,但人类,兽人和精灵有不同的大小/字段,不同的对齐要求和不同的vtable。在这种情况下,当客户端代码想要处理它们的非同类列表时,将多态基本指针存储到这样的生物:

for each creature in creatures:
     creature.do_something();

...而不是牺牲多态性的那个:

for each orc in orcs:
     orc.do_something();
for each human in humans:
     humans.do_something();
for each elf in elves:
     elves.do_something();

......每当我们介绍一种新型生物时,如果我们需要在许多地方进行此操作,这将是一个真正的PITA ...

...那么如果我们想要保持多态解决方案但仍然以非同类方式一次一个地处理每个生物,我们仍然最终会失去时间和空间位置,无论每个生物是否只是将backpointer存储到容器中或不存储。我们失去了vtable上的时间局部性,因为我们可能在一次迭代中访问一个vtable,然后在下一次迭代时访问另一个vtable。这里的内存访问模式也可以是随机的,零星的,导致空间局部性的丢失,因此我们最终会遇到缓存未命中。

因此,在这种情况下,如果您想要继承和多态,我的解决方案是在容器级别进行抽象:Orcs继承CreaturesHumans继承CreaturesElves继承Creatures。当它想要表达为特定生物执行的操作时,这会将一些额外的复杂性传递给客户端代码,但现在上面的顺序循环可以这样写:

for each creatures in creature_types:
     creatures.do_something();

在第一次迭代中,可能会对整个兽人列表(可能像存储在数组中的一百万个兽人)做一些事情。现在,该列表中的所有兽人都可以连续存储,并且我们将同类功能应用于该列表中的所有兽人。在这种情况下,我们有一大堆呼吸空间可以优化,而不会改变设计。

我们在这里仍然有一个非同质的循环利用多态性,但它现在是如此,便宜得多,因为我们只需支付整个容器生物的开销,而不是每个单独的生物。处理各个生物的循环现在是同质的。这类似于使用抽象IImage,例如,使一堆图像(一堆像素容器)变亮,而不是一次一个地实现IPixel的抽象像素对象。 / p>

同构循环和陈述

这样就可以将繁重的关键循环从非均匀循环中移开,从一个地方一次一个地处理各种不同的数据,并将它们移向均匀循环,处理连续存储的同类数据。

这是我看待界面设计的一般策略。如果它很容易以难以优化的方式产生热点,那么我认为固有的问题是接口的设计过于细化(Creature,而不是{{1 }})。

如果希望利用OOP,那就是我看到解决这个问题的方式。我认为您的设计理念类型可能有用的是简化客户端代码必须表达仅适用于一个特定生物的操作的情况,此时它们可以通过某种代理对象工作,该代理对象指向容器和也许存储一个指向特定条目的索引或指针以使其更方便,例如Creatures引用其中一个抽象CreatureHandle容器中的特定条目。