随着增加实体数量,物理代码变得越来越慢

时间:2018-11-23 06:13:48

标签: c++ multithreading oop caching

我在一次在线采访评估中遇到了这个问题。

您会注意到,随着增加实体数量,您的物理代码变得越来越慢。以下哪项可以帮助您?

typedef struct Entity_t{

double pos_x, pos_y;
double vel_x, vel_y;
int health, action, mind;
int level;
void *equipment, *abilities, *effects, 

}Entity;
  • A。将物理数据移动到数组结构中以增加缓存的一致性。
  • B。通过将设备,能力和等级移到实体中来减少指针间接。
  • C。重新排列数据,使pos_x,pos_y,vel_x和bel_y保持最后状态以提高访问速度。
  • D。将实体移到链接列表中,以便您可以直接访问当前的下一项。

我的猜测是D,但是我觉得这是不对的,因为在这种情况下,我认为更快地访问下一个项目并不有用。物理代码变慢的原因应该与缓存有关。 A与缓存有关,但是我不知道“将物理数据移动到结构中”是否可以提高缓存的一致性。我只知道用于缓存一致性的硬件解决方案。 B&C似乎都与问题无关。

3 个答案:

答案 0 :(得分:1)

这里有很多内容需要介绍,因为这会有点冗长,但是我觉得值得花时间和精力来阅读它。因此,我将从一些基本概念入手,然后介绍一些较困难的方面。我将从与数据结构及其字节对齐有关的代码设计开始,使用指针与智能指针进行比较,然后继续介绍容器及其相关算法。


个人而言,我不会有一堆单独的floatsdoubles来代表每个坐标位置以及坐标速度,加速度等。创建代表这两个坐标的简单类或结构会更容易点和向量取决于您如何使用它们。在您的特定情况下,您只能在2D空间中工作,而不是在3D空间中工作,但是该概念仍然适用;唯一的区别是可以对它们执行的数学运算,例如2D向量可以具有点积,但尽管确实存在,但通常没有定义明确的叉积,其中3D中点和叉积都存在存在,并且3D叉积定义明确。

还有许多其他与向量有关的有用函数,例如查找长度或大小,寻找向量的方向,获取单位向量,检查其是否为0向量以及所有基本算术一元和二进制形式的逻辑操作以及可以对其执行的逻辑操作:+-*\。对于*,请不要将它与叉号和点积相混淆。通常是将scalar乘以该向量,然后进行逻辑比较==!=<<=,{{1 }} ...与点积相关的其他一些常用函数也很常用,这些函数通过使用>=函数和cos来涉及点积的三角形式。 abs中的一个。因此,考虑到这一点,您可以轻松地从实体和所有其他位置对象推断所有点和矢量的功能,然后仅使用该类的单个变量来表示所需的内容。使数学向量非常有用的另一件事是,您不仅可以通过标量和另一个向量对这些向量执行运算,还可以通过矩阵对这些向量执行运算,从而可以对它们进行转换。尤其是仿射变换,旋转,平移,缩放...


Vector2f 类的示例:

magnitude

对于构造函数和函数本身,我将不在此处显示,因为它们将显示太多,而对于class Vector2f { public: union { // nameless union to designate both the array elements and the // individual elements have the same memory location: helps // with different way of accessing the individual vector components float f2_[2]; // internal array of float with size 2 struct { // nameless struct float x_; float y_; }; }; // Different ways to construct a vector inline Vector2f(); inline Vector2f( float x, float y ); inline Vector2f( float* vp ); // operators inline Vector2f operator+( const Vector2f& v2 ) const; inline Vector2f operator+() const; inline Vector2f& operator+=( const Vector2f& v2 ); inline Vector2f operator-( const Vector2f& v2 ) const; inline Vector2f operator-() const; inline Vector2f& operator-=( const Vector2f& v2 ); inline Vector2f operator*( const float& value ) const; inline Vector2f& operator*=( const float& value ); inline Vector2f operator/( const float& value ) const; // check for divide by 0 inline Vector2f& operator/=( const float& value ); // same as above // Common Functions inline void normalize(); inline void zero(); inline bool isZero(); // use an epsilon value inline float dot( const Vector2f v2 ) const; inline float lenght2() const; // two ways of calculating the length or magnitude inline float length() const; inline float getCosAngle( const Vector2f& v2, const bool isNormalized = false ); inline float getAngle( const Vector2f& v2, const bool isNormalized = false, bool inRadians = true ); inline friend Vector2f Vector2f::operator*( const float& value, const Vector2f v2 ) { return Vector2( value * v2.x_, value * v2.y_ ); } inline friend Vector2f Vector2f::operator/( const float& value, const Vector2f v2 ) { Vector2f vec2; if ( Math::isZero( v2.x_ ) ) vec2.x_ = 0.0f; else vec2.x_ = value / v2.x_; if ( Math::isZero( v2.y_ ) ) vec2.y_ = 0.0f; else vec2.y_ = value / v2.y_; return vec2; } }; 来说,它基本上是一种通用数学函数,用于检查值是否小于比某些epsilon值小,可以认为它可以忽略不计,并允许代码将其视为零(由于浮点算术和舍入错误)。


然后在您现有的课程中,您只需执行以下操作:

Math::isZero()

这很好,除了我们所有的矢量和点都可以是双精度和整数。我们还可以使用具有3个坐标位置的向量,因此可以通过将Vector2f类转换为模板类型来做得更好:


#include "Vector2f.h"

struct Stats {
    int health_;
    int mind_;
    int action_;
    int level_;
};

// Since this is C++ and not C there is no need for `typedef` `structs` although it is still valid C++
struct Entity {
    Vector2f position_;
    Vector2f velocity_;
    Stats stats_;

    void *equipment, *abilities, *effects;
}; 

更妙的是,您可以使用一个非常易于使用的库来代替2D和3D类型的应用程序,它们的库具有OpenGL和GLSL的感觉,template<class Type, unsigned N> // Type is float, double, int, etc. N is number of dimensions class Vector { // class body here: }; 而不是编写整个向量类, 。您可以找到它here,因为它非常易于使用和安装,因为它是仅glm的库,没有链接。只需包含标题即可开始使用它,因为它功能强大且用途广泛。

使用数学矢量类如此有用的原因是,许多不同类型的对象可以具有位置,速度或加速度。您甚至可以使用它们来包含网格数据,例如网格的顶点坐标,纹理坐标,颜色数据,例如来自headers库的数据:glm会有glm::vec4<float> color红色,绿色,蓝色和Alpha。 r,g,b,a将具有glm::vec2<float> textureCoords坐标的位置。最强大的部分是能够简单地对带有标量的向量,其他向量和矩阵进行数学运算。


关于涉及u, v的实际问​​题的最佳建议是,确保您创建的结构具有cache coherency对齐方式。因此,如果您具有这样的结构:

4 byte

上面的结构在4字节对齐时将失败,因此要解决此问题:

struct Foo {
    int a;   // assuming 32bit = 4bytes
    short b; // assuming 16bit
};

这将有助于解决您的缓存问题。它们的存储顺序很重要,因为它取决于连续对齐的变量,这些变量与32位计算机上通常找到的4个字节对齐。我不知道对于4字节对齐的64位计算机是否同样适用,但这是要考虑的问题。需要考虑的另一件事是,某个结构Foo的单词对齐方式可能在一个OS与另一个OS之间以及从一个编译器与另一个OS都不相同。还要考虑的另一件事是具有数据类型内部布局的体系结构,例如字节序,但这通常仅在您以特定方式使用struct Foo { int a; short b; private: short padding; // not used }; unions并执行按位操作-操作。


回到代码的这种变体:

bit fields

因此,这里涉及#include "Vector2f.h" struct Stats { // assuming 32bit int health_; // 4 bytes int mind_; // 4 bytes int action_; // 4 bytes int level_; // 4 bytes }; struct Entity { Vector2d position_; // float = 8 bytes x 2: 16 bytes Vector2d velocity_; // float = 8 bytes x 2: 16 bytes Stats stats_; // 32 bytes void *equipment, *abilities, *effects; // each pointer 4 bytes. }; 的实体类都属于4字节对齐,因为它们都是4的倍数,通常在大多数情况下,这是缓存友好的。


您的指针涉及间接,分配和释放。您有几种选择。只要引用的寿命比当前类的寿命长(预期寿命),就可以将其传递给对象,以避免使用variablesnew。您可以使用delete。您可以将它们包含在smart pointers中,而根本不使用指针,可以将所有这些指针放置在它们自己的结构中,然后在类中只有一个指向该结构的指针,或者可以做这样的事情:

std::vector

以下是在元组中使用class Entity { private: Vector<float,2> position_; Vector<float,2> velocity_; Stats stats_; std::tuple<Equipment*, Ability*, Effect*> attributes_; }; 的简单示例:

class pointers

但是,您必须确保以前的对象的生命周期:#include <exception> #include <iostream> #include <tuple> class Equipment { public: int x; }; class Ability { public: int x; }; class Effect { public: int x; }; class Player { public: std::tuple<Equipment*, Ability*, Effect*> attributes; }; int main() { try { Equipment eq; eq.x = 5; Ability ab; ab.x = 7; Effect ef; ef.x = -3; Player p; p.attributes = std::make_tuple<Equipment*, Ability*, Effect*>( &eq, &ab, &ef ); std::cout << "Equipment: " << std::get<0>( p.attributes )->x << '\n'; std::cout << "Ability: " << std::get<1>( p.attributes )->x << '\n'; std::cout << "Effect: " << std::get<2>( p.attributes )->x << '\n'; } catch( std::runtime_error& e ) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; } 生存在Equipment, Ability, EffectEntity对象中,该对象才能正常工作。在某些方面,这也可能是无效的,因为玩家可能包含以上每个中的一个以上。因此,最简单的处理方式是每个容器都有一个Player容器:

std::vector

如果您确实需要指针,那么可以有以下内容:

 class Entity {
 private:
     Vector<double, 2> position_;
     Vector<double, 2> velocity_;
     Stats stats_;
     std::vector<Equipment> inventory_;
     std::vector<Ability>   abilities_;
     std::vector<Effect>    effects_;
};

在课堂上。然后,您将拥有一个智能指针容器,它将处理您的所有分配和删除操作,如果您不需要共享指针信息,则可以使用 std::vector<std::shared_ptr<T>> objects_;


设计std::unique_ptr<T>并知道其布局后,就可以设计一种适合您需求的算法,或者选择已经存在的合适算法。由Data Structuresstl等任何常用的库编写。


我知道这很长,但是我想我已经涵盖了您建议的所有主题,甚至有可能选择您建议的主题的组合,但是每个主题都有其取舍,因为总是对您所做的特定事情的利弊。提高容器速度查找的代价是更多的内存和直接访问的损失,而使用的内存更少,查找速度更快但插入速度却慢得多。

知道何时何地使用哪种容器以及适当的算法是使应用程序高效且无缺陷运行的关键。

希望;这可能会对您有所帮助。

答案 1 :(得分:1)

期望的答案是B。动态分配的指针数量的增加会增加高速缓存未命中的次数,但更重要的是,连续的动态[de]分配会大大降低程序的速度。 但是这个答案是主观的。没有完整的最小代码示例,就无法提供准确的解决方案,也可能会考虑其他选择。

答案 2 :(得分:1)

我敢肯定,唯一可以做相反的事情就是选项D。您已经有了一种访问所有实体的方法,这也许就很好了。添加下一个指针会使每个实体变大,因此缓存命中率将降低。另外,您还添加了另一个间接方式,因此速度也较慢。

对于所有其他选项,结果取决于代码。

如果没有大量创建/销毁实体并线性处理实体,则选项A可以肯定。但是,如果代码创建或破坏了实体,则通常必须复制数组。

选项B:例如,如果许多实体共享同一设备,则将其移入结构意味着重复大量数据。不能用于缓存。另一方面,如果设备是唯一的,则额外的间接操作无济于事。

选项C:这似乎是一个完整的废话答案。最后没有改变访问速度。另一方面,移动成员会极大地影响速度。一起使用的成员应移动,以便它们位于同一高速缓存行中。由于访问会将一条完整的缓存行加载到缓存中,并且您希望减少缓存行的数量,因此该算法需要频繁加载。