类型是否应该采用面向数据的设计方法?

问题描述 投票:3回答:1

目前,我的应用程序包含三种类。它应该遵循面向数据的设计,如果不是,请纠正我。这些是三类。代码示例并不重要,如果需要,可以跳过它们。他们只是给人一种印象。我的问题是,我应该在类型类中添加方法吗?

Current design

类型只是持有价值。

struct Person {
    Person() : Walking(false), Jumping(false) {}
    float Height, Mass;
    bool Walking, Jumping;
};

模块各自实现一个独特的功能。他们可以访问所有类型,因为它们全局存储。

class Renderer : public Module {
public:
    void Init() {
        // init opengl and glew
        // ...
    }
    void Update() {
        // fetch all instances of one type
        unordered_map<uint64_t, *Model> models = Entity->Get<Model>();
        for (auto i : models) {
            uint64_t id = i.first;
            Model *model = i.second;
            // fetch single instance by id
            Transform *transform = Entity->Get<Transform>(id);
            // transform model and draw
            // ...
        }
    }
private:
    float time;
};

管理者是通过基础Module类注入模块的助手。上面使用的Entity是实体管理器的一个实例。其他经理包括消息传递,文件访问,SQL存储等。简而言之,应该在模块之间共享的每个功能。

class ManagerEntity {
public:
    uint64_t New() {
        // generate and return new id
        // ...
    }
    template <typename T>
    void Add(uint64_t Id) {
        // attach new property to given id
        // ...
    }
    template <typename T>
    T* Get(uint64_t Id) {
        // return property attached to id
        // ...
    }
    template <typename T>
    std::unordered_map<uint64_t, T*> Get() {
        // return unordered map of all instances of that type
        // ...
    }
};

Problem with it

现在你已经了解了我目前的设计。现在考虑一种类型需要更复杂的初始化的情况。例如,Model类型只为其纹理和顶点缓冲区存储OpenGL ID。实际数据必须先上传到视频卡。

struct Model {
    // vertex buffers
    GLuint Positions, Normals, Texcoords, Elements;
    // textures
    GLuint Diffuse, Normal, Specular;
    // further material properties
    GLfloat Shininess;
};

目前,有一个Models模块具有Create()功能,负责设置模型。但是这样,我只能从这个模块创建模型,而不能从其他模块创建模型。我应该在复杂化时将它移动到类型类Model吗?我将类型定义作为之前的接口。

c++ architecture encapsulation data-oriented-design
1个回答
4
投票

首先,您不一定需要在任何地方应用面向数据的设计。它最终是一种优化,即使是性能关键的代码库仍然有很多部分不能从中受益。

我倾向于将其视为具有删除结构的结构,而不是处理更有效的大数据块。举个例子拍一张照片。为了有效地表示其像素,通常需要存储简单的数值数组,而不是例如用户定义的抽象像素对象的集合,其具有虚拟指针作为夸大的示例。

想象一下使用浮点数的4分量(RGBA)32位图像,但出于任何原因仅使用8位alpha(对不起,这是一个愚蠢的例子)。如果我们甚至使用基本的struct作为像素类型,由于对齐所需的结构填充,我们通常最终会使用像素结构需要更多的内存。

struct Image
{
    struct Pixel
    {
        float r;
        float g;
        float b;
        unsigned char alpha;
        // some padding (3 bytes, e.g., assuming 32-bit alignment
        // for floats and 8-bit alignment for unsigned char)
    };
    vector<Pixel> Pixels;
};

即使在这种简单的情况下,将其转换为具有8位alpha并行阵列的平面浮点数也会减小内存大小,从而可能提高顺序访问速度。

struct Image
{
    vector<float> rgb;
    vector<unsigned char> alpha;
};

......这就是我们最初应该思考的问题:关于数据,内存布局。当然,图像通常已经被有效地表示,并且已经实现了图像处理算法以批量处理大量像素。

然而,面向数据的设计通过将这种表示应用于比像素高得多的事物,将其比平常更进一步。以类似的方式,你可能会受益于建模ParticleSystem而不是单个Particle,为优化留下这样的喘息空间,甚至People而不是Person

但是让我们回到图像示例。这往往意味着缺乏国防部:

struct Image
{
    struct Pixel
    {
        // Adjust the brightness of this pixel.
        void adjust_brightness(float amount);

        float r;
        float g;
        float b;
    };
    vector<Pixel> Pixels;
};

这种adjust_brightness方法的问题在于,从界面的角度来看,它是针对单个像素进行设计的。这使得难以应用优化和算法,这些优化和算法可以同时访问多个像素而受益。同时,这样的事情:

struct Image
{
    vector<float> rgb;
};
void adjust_brightness(Image& img, float amount);

...可以通过一次访问多个像素的方式编写。我们甚至可以用SoA代表代表它:

struct Image
{
    vector<float> r;
    vector<float> g;
    vector<float> b;
};

...如果您的热点与顺序处理有关,那么这可能是最佳的。细节并不重要。对我来说重要的是你的设计留下了喘息的空间来优化。对我来说,DOD的价值实际上是如何预先考虑这种类型的思想将为您提供这些类型的界面设计,让您在以后根据需要进行优化,而无需进行侵入式设计更改。

多态性

多态性的典型例子往往也集中在那种粒状的一次性思维模式上,比如Dog继承了Mammal。在游戏中,有时会导致开发人员开始不得不与类型系统作斗争的瓶颈,按子类型排序多态基本指针以改善vtable上的临时位置,尝试使数据成为特定的子类型(Dog,例如)连续分配自定义分配器用于改善每个子类型实例的空间局部性等。

如果我们在较粗糙的水平上建模,那么这些负担都不需要存在。你可以让Dogs继承抽象的Mammals。现在,虚拟调度的成本降低到每种哺乳动物一次,而不是每个哺乳动物一次,并且所有特定类型的哺乳动物都可以有效且连续地表示。

您仍然可以获得所有想象力,并将DOP和多态性与DOD思维模式结合使用。诀窍是确保你设计的东西足够粗糙,这样你就不会试图与类型系统作斗争并解决数据类型以重新控制内存布局等问题。如果你设计的东西足够粗糙,你就不必费心去做。

界面设计

至少就我看来,DOD仍然涉及到界面设计,你可以在你的课程中使用方法。设计合适的高级接口仍然非常重要,您仍然可以使用虚拟功能和模板,并且非常抽象。我所关注的实际差异在于,您设计了聚合接口,就像上面的adjust_brightness方法一样,这为您提供了优化的喘息空间,而不会在整个代码库中进行级联设计更改。我们设计了一个界面来处理整个图像的多个像素,而不是一次处理一个像素的像素。

DOD接口设计通常被设计为批量处理,并且通常以对于必须访问所有内容的大多数性能关键的线性复杂性顺序循环具有最佳存储器布局的方式。

因此,如果我们以Model为例,那么缺少的是接口的聚合方面。

struct Models {
    // Methods to process models in bulk can go here.

    struct Model {
        // vertex buffers
        GLuint Positions, Normals, Texcoords, Elements;
        // textures
        GLuint Diffuse, Normal, Specular;
        // further material properties
        GLfloat Shininess;
    };

    std::vector<Model> models;
};

这并不一定要使用带方法的类来表示。它可能是一个接受structs数组的函数。这些细节并不重要,重要的是界面主要设计为批量处理,而数据表示则针对该情况进行了最佳设计。

热/冷分裂

看看你的Person类,你可能仍然会在某种经典界面中考虑某种方式(即使这里的界面只是数据)。同样,只有当这是大多数性能关键环路的最佳内存配置时,DOD才会主要使用struct。它不是关于人类的逻辑组织,而是关于机器的数据组织。

struct Person {
    Person() : Walking(false), Jumping(false) {}
    float Height, Mass;
    bool Walking, Jumping;
};

首先让我们把它放在上下文中:

struct People {
    struct Person {
        Person() : Walking(false), Jumping(false) {}
        float Height, Mass;
        bool Walking, Jumping;
     };
};

在这种情况下,所有字段是否经常一起访问?假设,假设答案是否定的。这些WalkingJumping字段有时仅访问(冷),而HeightMass一直被访问(热)。在这种情况下,可能更优化的表示可能是:

struct People {
    vector<float> HeightMass;
    vector<bool> WalkingJumping;
};

当然,你可以在这里制作两个独立的结构,有一个指向另一个,等等。关键是你最终从内存布局/性能的角度来设计它,理想情况下你手中有一个好的分析器,并且对常见的用户端代码路径。

从界面的角度来看,您设计的界面侧重于处理人,而不是人。

问题

有了这个,你的问题:

我只能从这个模块创建模型,而不能从其他模块创建。我应该在复杂化时将它移动到类型类吗?

这更像是一种子系统设计问题。由于你的Model代表是关于OpenGL数据的,它应该属于可以正确初始化/销毁/渲染它的模块。它甚至可能是此模块的私有/隐藏实现细节,此时您在模块的实现中应用DOD思维模式。

然而,外部世界可用于添加模型,破坏模型,渲染它们等的界面最终应该被设计为批量。可以把它想象成为容器设计一个高级接口,其中你想要为每个元素添加的方法最终都属于容器,就像上面的adjust_brightness图像示例一样。

复杂的初始化/销毁通常需要一次一个的设计心态,但关键是你通过聚合接口来实现这一点。在这里你可能仍然放弃Model的标准构造函数和析构函数,支持初始化添加GPU Model进行渲染,清除GPU资源以从列表中删除它。它有点回到个人类型(例如人)的C风格编码,尽管你仍然可以使用C ++好东西来获得聚合接口(例如人)。

我的问题是,我应该在类型类中添加方法吗?

主要是为散装设计,你应该在路上。在您展示的示例中,通常没有。它不一定是最难的规则,但你的类型是为个别事物建模,为DOD留出空间通常需要缩小并设计处理许多事物的界面。

© www.soinside.com 2019 - 2024. All rights reserved.