我正在读这篇文章,这个家伙继续谈论如何将面向数据的设计与面向对象的设计结合起来,每个人都能从中受益。但是,他没有展示任何代码示例。
我在谷歌上搜索了一下,找不到任何关于这是什么的真实信息,更不用说任何代码示例了。有谁熟悉这个术语并能提供一个例子吗?这个词是不是有别的意思?
我正在读这篇文章,这个家伙继续谈论如何将面向数据的设计与面向对象的设计结合起来,每个人都能从中受益。但是,他没有展示任何代码示例。
我在谷歌上搜索了一下,找不到任何关于这是什么的真实信息,更不用说任何代码示例了。有谁熟悉这个术语并能提供一个例子吗?这个词是不是有别的意思?
在面向数据的设计中,应用程序的逻辑是由数据集而不是过程算法构建的。例如
程序的方法。
int animation; // this value is the animation index
if(animation == 0)
PerformMoveForward();
else if(animation == 1)
PerformMoveBack();
.... // etc
数据设计方法
typedef struct
{
int Index;
void (*Perform)();
}AnimationIndice;
// build my animation dictionary
AnimationIndice AnimationIndices[] =
{
{ 0,PerformMoveForward }
{ 1,PerformMoveBack }
}
// when its time to run, i use my dictionary to find my logic
int animation; // this value is the animation index
AnimationIndices[animation].Perform();
这样的数据设计促进了使用数据来构建应用程序的逻辑。它更容易管理,特别是在电子游戏中,因为它可能有数千条基于动画或其他因素的逻辑路径。
首先,不要将其与数据驱动设计混淆。
我对面向数据设计的理解是,它是关于组织数据以进行有效处理。特别是在缓存丢失等方面。另一方面,数据驱动设计是关于让数据控制程序的许多行为(Andrew Keith的回答很好地描述了这一点)。
假设您的应用程序中有球状对象,具有颜色、半径、弹性、位置等属性。
面向对象方法
在OOP中,你可以这样描述球:
class Ball {
Point position;
Color color;
double radius;
void draw();
};
然后你会创建一个像这样的球的集合:
vector<Ball> balls;
面向数据的方法
然而,在面向数据的设计中,你更有可能像这样编写代码:
class Balls {
vector<Point> position;
vector<Color> color;
vector<double> radius;
void draw();
};
正如你所看到的,不再有一个单位代表一个球了。球状物体只隐式存在。
这在性能方面有很多优势。通常,我们希望同时对多个球进行运算。硬件通常需要大量连续的内存块来有效地运行。
其次,你可以做一些只影响球的部分性质的操作。例如,如果你以各种方式组合所有球的颜色,那么你希望你的缓存只包含颜色信息。然而,当所有的球属性都存储在一个单元时,你也会把球的所有其他属性都拉进来。即使你不需要他们。
缓存使用实例
假设每个球占用64个字节,一个点占用4个字节。一个缓存槽也需要64字节。如果我想要更新10个球的位置,我必须将10 x 64 = 640字节的内存拉入缓存,并获得10次缓存失败。然而,如果我可以将球的位置作为单独的单位,这将只需要4 x 10 = 40字节。它适合一次缓存读取。因此我们只有1次缓存失误来更新所有的10个球。这些数字是任意的——我假设缓存块更大。
但它说明了内存布局如何对缓存命中和性能产生严重影响。随着CPU和RAM速度差异的扩大,这只会增加其重要性。
如何布局内存
在我的球的例子中,我简化了很多问题,因为通常对于任何普通的应用程序,你可能会同时访问多个变量。例如,位置和半径可能会经常一起使用。那么你的结构应该是:
class Body {
Point position;
double radius;
};
class Balls {
vector<Body> bodies;
vector<Color> color;
void draw();
};
您应该这样做的原因是,如果一起使用的数据被放置在单独的数组中,那么它们将有竞争缓存中相同插槽的风险。这样一来,装载一个就会丢掉另一个。
因此,与面向对象编程相比,您最终创建的类与问题的心理模型中的实体无关。由于数据是根据数据的使用情况集中在一起的,所以在面向数据的设计中,并不总是有合理的名称来给类命名。
与关系数据库的关系
The thinking behind Data-Oriented Design is very similar to how you think about relational databases. Optimizing a relational database can also involve using the cache more efficiently, although in this case, the cache is not CPU cache but pages in memory. A good database designer will also likely split out infrequently accessed data into a separate table rather than creating a table with a huge number of columns where only a few of the columns are ever used. He might also choose to denormalize some of the tables so that data don't have to be accessed from multiple locations on disk. Just like with Data-Oriented Design these choices are made by looking at what the data access patterns are and where the performance bottleneck is.
我只是想指出Noel特别谈到了我们在游戏开发中所面临的一些特定需求。我认为其他正在进行实时软模拟的部门将从中受益,但它不太可能是一种对一般业务应用程序有显著改进的技术。这种设置是为了确保从底层硬件中挤出最后一点性能。
Mike Acton最近做了一个关于面向数据设计的公开演讲:
我对它的基本总结是:如果你想要性能,那么就考虑数据流,找到最有可能破坏你的存储层,然后努力优化它。Mike专注于L2缓存丢失,因为他做的是实时的,但我想同样的事情也适用于数据库(磁盘读取)甚至Web (HTTP请求)。我认为这是一种很有用的系统编程方法。
请注意,它并没有免除您对算法和时间复杂性的思考,它只是将您的注意力集中在找出最昂贵的操作类型上,然后您必须以您疯狂的CS技能为目标。
如果想要利用现代处理器架构,就需要以某种方式在内存中布局数据。cpu非常擅长处理在内存中按顺序排列的简单类型。任何其他布局的处理成本都高得多。
在面向对象的方法中,您总是考虑一个实例,然后通过将对象分组到集合中将其扩展到多个实例。但从硬件的角度来看,这带来了额外的成本。
在面向数据的方法中,你不像在面向对象编程中那样有一个“实例”。您的实例可以有一个标识符,类似于关系数据库中的数据,但除此之外,与您的实例相关的数据可以分割到几个表中(表被实现为向量),以实现高效的处理。
举个例子:假设你有一个类Student {int id;std:: string名称;平均浮动;bool毕业;}。在面向对象编程中,你可以把所有的学生放在一个向量中。
在面向数据的设计中,首先要问自己希望对这些数据进行什么样的处理。假设你想计算所有尚未毕业的学生的平均分数。因此,您将创建一个表,其中只包含已毕业的学生和未毕业的学生。您不会在该表中保留学生名,因为它不用于处理。但是你会保留一个学号和一个平均分。
现在计算非毕业学生的平均分意味着遍历非毕业表并执行计算。由于平均标记在内存中是相邻的,您的CPU将使用SIMD并以最有效的方式处理数据。由于我们没有查询已毕业的bool值来测试学生是否已经毕业,因此不会出现数据缓存丢失的情况。
这在理论上听起来不错,但我从未在现实项目中进行过这种开发。如果任何人有任何经验,请联系我,我有很多问题。
我第一次听说面向数据的设计是在我们的机械播客“S3: EP4面向数据的设计”那一集。https://www.owltail.com/podcast/Atvr2-Our-Machinery
也许这种情况已经改变了,但在一段时间以前,找到关于面向数据设计的信息是很困难的。我找到的唯一一本书是:https://www.manning.com/books/data-oriented-programming