【问题标题】:How do I implement a data oriented design in Rust?如何在 Rust 中实现面向数据的设计?
【发布时间】:2017-02-06 20:50:09
【问题描述】:

背景

在游戏引擎开发中,我们通常使用面向数据的设计来优化内存和计算性能。

我们以粒子系统为例。

在一个粒子系统中,我们有很多粒子,每个粒子可能有几个属性,比如位置、速度等。

一个典型的 C++ 实现是这样的:

struct Particle {
    float positionX, positionY, positionZ;
    float velocityX, velocityY, velocityZ;
    float mass;
    // ...
};

struct ParticleSystem {
    vector<Particle> particles;
    // ...
};

这个实现的一个问题是粒子属性是相互交错的。 这种内存布局对缓存不友好,可能不适合 SIMD 计算。

在面向数据的设计中,我们编写以下代码:

struct ParticleAttribute {
    size_t size;
    size_t alignment;
    const char* semantic;
};

struct ParticleSystem {
    ParticleSystem(
        size_t numParticles,
        const ParticleAttribute* attributes,
        size_t bufferSize) {
        for (size_t i = 0; i < numAttributes; ++i) {
            bufferSize += attributes[i].size * numParticles;
            // Also add paddings to satisfy the alignment requirements.
        }
        particleBuffer = malloc(bufferSize); 
    }

    uint8* getAttribute(const char* semantic) {
        // Locate the semantic in attributes array.
        // Compute the offset to the starting address of that attribute.
    }

    uint8* particleBuffer;      
};

现在我们只有一个分配,每个属性都连续驻留在内存中。 为了模拟粒子,我们可以编写如下代码:

symplecticEuler(ps.getAttribute("positionX"), ps.getAttribute("velocityX"), dt);

getAttribute 函数将获取特定属性的起始地址。

问题

我想知道如何在 Rust 中实现这一点。

我的想法是首先创建一个名为ParticleSystem的类,它需要几个ParticleAttributes来计算总缓冲区大小, 然后为缓冲区分配内存。 我认为这可以在 Rust 安全代码中完成。

下一步是实现getAttribute函数,它 将返回对特定属性起始地址的引用。 我在这里需要你的帮助。 如何获取带有偏移量的原始地址并将其转换为所需的类型(例如 float*)并将该原始指针包装到 Rust 中的可变引用?

此外,我认为我应该将该原始指针包装到对数组的可变引用,因为我需要使用 SIMD 库通过该引用加载四个元素。 如何使用 Rust 实现这一点?


更新:提供有关属性的更多信息。 属性的数量和详细信息在运行时确定。 属性的类型可以有所不同,但我认为我们只需要支持原始类型(f32、f64、ints、...)。

【问题讨论】:

    标签: rust data-oriented-design


    【解决方案1】:

    这是实现 DOD 的一种非常复杂的方式,使用运行时查找获取 getter 的想法让我畏缩不前。

    简单的版本是简单地为每个属性分配一个内存:

    struct Particles {
        x: Vec<f32>,
        y: Vec<f32>,
    }
    

    这需要事先知道属性。

    那么就没有什么恶作剧了,他们只是坐在那里,已经打字,等着你。


    将其扩展到动态确定的属性并不复杂:

    • 我们可以使用HashMap&lt;String, xxx&gt; 在运行时查找给定属性
    • 我们可以使用enum 将单个Value 存储在可以采用多种形式的哈希映射中(另一种解决方案是使用特征)

    这就变成了:

    #[derive(Debug, Hash, PartialEq, Eq)]
    enum Value {
        UniformInt(i64),
        UniformFloat32(f32),
        UniformFloat64(f64),
        DistinctInt(Vec<i64>),
        DistinctFloat32(Vec<f32>),
        DistinctFloat64(Vec<f64>),
    }
    
    struct Particles {
        store: HashMap<String, Value>,
    }
    

    我们也可以使用 6 个哈希映射...但是除非先验地知道类型是什么(当唯一的想法是字符串时),否则必须一次查看所有哈希映射:烦人,而且浪费时间。

    【讨论】:

    • 感谢您的快速回复。我采用这种复杂的方式来实现 DOD,因为需要属性的灵活性。通常详细的属性信息(即属性的数量和确切的语义)来自艺术家。换句话说,这些信息是动态的,在运行前无法确定。
    • @TheBusyTypist:我明白了...请使用相关详细信息编辑问题。同样值得知道您是否只需要支持f32(在这种情况下HashMap&lt;String, Vec&lt;32&gt;&gt; 会很好地工作)或者您是否需要支持多种类型。
    • 我必须支持其他原始类型,例如 f64ints(用于粒子 ID)。正如您所建议的,现在我认为按类型将属性分组在单独的 HashMap 中可能是一个有效的解决方案。
    • HashMap 解决方案几乎可以满足所有要求,但它仍然缺乏一些更精细的控制。一个这样的例子是区分统一属性和可变属性。通过统一属性,我的意思是属性值在所有粒子之间是一个常数,而对于可变属性它可以是不同的。我们可以只存储一个统一属性的值。在我们的HashMap 解决方案中,我们需要添加另一个键来区分它们。恐怕在更复杂的情况下会发生组合爆炸。
    • 我认为enums 可以在这里挽救一天:enum Value { UniformInt(u64), UniformFloat32(f32), UniformFloat64(f64), VariableInt(Vec&lt;u64&gt;), VariableFloat32(Vec&lt;f32&gt;), VariableFloat64(Vec&lt;f64&gt;) } 然后HashMap&lt;String, Value&gt;
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-21
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 2020-11-21
    • 2011-07-10
    相关资源
    最近更新 更多