физический код становится медленнее с увеличением количества объектов - PullRequest
0 голосов
/ 23 ноября 2018

Я встречал этот вопрос в ходе онлайн-интервью.

вы замечаете, что ваш физический код замедляется по мере увеличения количества сущностей.что из следующего может помочь?

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.Уменьшите косвенность указателя путем перемещения оборудования, способностей и уровня в Entity.
  • C.Переставьте данные так, чтобы pos_x, pos_y, vel_x и bel_y были последними для улучшения скорости доступа.
  • D.Переместите объекты в связанный список, чтобы получить прямой доступ к следующему элементу из текущего.

Я предполагаю, что D, но я чувствую, что это неправильно, потому что я не думаю, что более быстрый доступ к следующему пункту полезен в этом сценарии.Причина, по которой код физики становится медленнее, связана с кэшированием.А относится к кешированию, но я не знаю, может ли «перенести физические данные в структуру» повысить когерентность кеша.Я знаю только аппаратное решение для согласованности кэша.оба B & C, похоже, не связаны с вопросом.

Ответы [ 3 ]

0 голосов
/ 23 ноября 2018

Ожидаемый ответ - B. Увеличение числа динамически размещаемых указателей увеличивает пропускную способность кэша, но, что более важно, постоянные динамические [de] выделения значительно замедляют программу.Но этот ответ субъективен;без полного минимального образца кода точное решение не может быть предоставлено, и другие варианты также могут быть рассмотрены.

0 голосов
/ 23 ноября 2018

Я почти уверен, что единственное, что сделает противоположное, - это вариант D. У вас уже есть способ получить доступ ко всем сущностям, что, вероятно, вполне нормально.Добавление следующего указателя сделает каждый объект больше, поэтому попадания в кэш уменьшатся.Кроме того, вы добавляете еще одно косвенное указание, чтобы оно тоже было медленнее.

Для всех других вариантов результат зависит от кода.

Опция A в значительной степени верна, если сущности не создаются / уничтожаются многои обрабатывается линейно.Но если код создает или уничтожает сущности, вам часто приходится копировать массив вокруг.

Вариант B: например, если много сущностей используют одно и то же оборудование, то перемещение его в структуру означает дублирование данных много времени.Не может быть хорошо для кеша.С другой стороны, если оборудование уникально, дополнительная косвенность не помогает.

Вариант C: Это, кажется, полный бессмысленный ответ.Ничто в последнем не меняет скорость доступа.С другой стороны, перемещение члена может оказать огромное влияние на скорость.Члены, которые используются вместе, должны быть перемещены, чтобы они находились в одной строке кэша.Поскольку access загружает в кеш полную строку кеша, а вы хотите минимизировать количество строк кеша, алгоритм должен загружаться часто.

0 голосов
/ 23 ноября 2018

Здесь есть кое-что, что будет немного длинно, но я чувствую, что стоит потратить время и силы на его прочтение.Итак, я начну с некоторых основных концепций и перейду к чуть более сложным аспектам.Я начну с разработки кода, который относится к структурам данных и их байтовым выравниваниям, использования указателей и интеллектуальных указателей, а затем перейду к контейнерам и связанным с ними алгоритмам.


Лично у меня не было бы группы отдельных floats или doubles для представления каждой координатной позиции и скорости координат, ускорения и т. Д. Было бы проще создать простой класс или структуру, котораяпредставляет точки и векторы в зависимости от того, как вы их используете.В вашей конкретной ситуации вы работаете только в 2D-пространстве, а не в 3D-пространстве, но концепция все еще применяется;единственное отличие состоит в том, что математические операции, которые могут быть выполнены над ними, например, двухмерный вектор может иметь точечное произведение, но, как правило, не имеет четко определенного перекрестного произведения, хотя оно существует, в то время как в 3D как точечное, так и перекрестное произведениесуществует, и перекрестный продукт в 3D хорошо определен.

Существует много других полезных функций, связанных с векторами, таких как определение длины или величины, определение направления вектора, получение единичного вектора, проверка, является ли это вектором 0, и всеосновные арифметические и логические операции, которые могут быть с ними выполнены: +, -, *, \ как в унарном, так и в двоичном виде.И для * это не следует путать с перекрестными и точечными произведениями;обычно это будет взятие scalar и умножение его на этот вектор, и для ваших логических сравнений ==, !=, <, <=, >= ... Есть также несколько других общихфункции, которые широко используются в отношении точечного произведения, которое включает в себя тригонометрическую форму точечного произведения посредством использования функции cos и abs ее magnitude.Таким образом, имея это в виду, вы можете легко экстраполировать все функциональные возможности точек и векторов из сущности и всех других позиционных объектов, а затем просто иметь одну переменную этого класса для представления того, что необходимо.Еще одна вещь, которая делает математические векторы очень полезными, заключается в том, что вы можете выполнять операции над этими векторами не только скалярным и другим вектором, но также и матрицами, где это позволит выполнять преобразования для них;особенно аффинные преобразования, вращение, перевод, масштабирование ...


Пример Vector2f класса:

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;
    }        
};

Что касается конструкторов иСами функции, я не собираюсь их здесь показывать, поскольку их было бы слишком много для отображения, а что касается Math::isZero(), который в основном является обобщенной математической функцией, которая проверяет, является ли значение меньше некоторого значения эпсилона, чтобы считать его незначительным и разрешитькод для обработки его как нулевого из-за арифметики с плавающей запятой и ошибок округления.


Тогда в вашем существующем классе вы можете просто сделать это:

#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;
}; 

Это нормально, но все, кроме нас, могут иметь векторы и точки, которые также имеют двойную точность и основаны на целых числах.У нас также могут быть векторы, имеющие 3 координатных положения, чтобы мы могли добиться большего успеха, превратив наш класс Vector2f в тип шаблона:


template<class Type, unsigned N>  // Type is float, double, int, etc. N is number of dimensions
class Vector {
    // class body here:      
};

Еще лучше, вместо записи всего вектораВ этом классе вы можете использовать очень простую в использовании библиотеку, которая очень хорошо работает с приложениями типа 2D и 3D, где их библиотека имеет вид OpenGL и GLSL под названием glm.Вы можете найти его здесь , так как он очень прост в использовании и установке, так как это библиотека headers и нет ссылок.Просто включите заголовки и начните использовать его, так как он очень мощный и универсальный.

Что делает использование математического векторного класса таким полезным, так это тот факт, что многие различные типы объектов могут иметь положение, скорость или ускорение.Вы даже можете использовать их для хранения данных сетки, таких как координаты вершины сетки, координаты текстуры, данные цвета, например, из библиотеки glm: a glm::vec4<float> color будет иметь r,g,b,a красный, зеленый, синий и альфа.Где glm::vec2<float> textureCoords будет иметь u, v координаты.И самая мощная часть этого - способность просто выполнять математику для векторов со скалярами, другими векторами и матрицами.


Лучшим предложением относительно вашего фактического вопроса, который включает cache coherency, было бы убедиться, что ваша структура, которую вы создаете, имеет выравнивание 4 byte.Поэтому, если у вас есть такая структура:

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

Приведенная выше структура потерпит неудачу при 4-байтовом выравнивании, чтобы исправить это:

struct Foo {
    int a;
    short b;
private:
    short padding; // not used
};

Это поможет решить проблемы с кешем,Порядок, в котором они хранятся, имеет значение, так как он зависит от последовательности переменных, которые выровнены с 4 байтами, обычно встречающимися на 32-битных машинах.Я не знаю, верно ли то же самое для 64-битных машин с 4-байтовым выравниванием, но это то, что нужно учитывать.Еще одна вещь, которую полезно учитывать, заключается в том, что выравнивание слов для некоторой структуры Foo может отличаться в разных ОС и в разных компиляторах.Еще одна вещь, которую следует принять во внимание, - это также архитектура с внутренним расположением типа данных, например его порядком байтов, но это обычно вызывает беспокойство, если вы используете 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.
}; 

Итак, здесь ваш класс сущностей, где речь идет о variables, все находятся на 4-байтовом выравнивании, так как все они кратныиз 4 и, как правило, в большинстве случаев это дружественный кеш.


Там, где ваши указатели связаны с косвенным указанием, распределением и освобождением.У вас есть несколько вариантов.Вы можете передать ссылку на объект, если ссылка живет дольше, чем текущий класс (ожидаемая продолжительность жизни), чтобы избежать использования new и 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, Effect из жизни вашего Entity или Player объекта, чтобы это сработало.Это также может быть неэффективным в некоторых отношениях, потому что игрок может содержать более 1 каждого из вышеперечисленных.Таким образом, самый простой способ сделать это - иметь контейнер 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 Structures и знаете, как они устроены, вам нужно либо разработать алгоритм, который соответствует вашим потребностям, либо выбрать подходящий уже существующий алгоритм, который уже был написан из любогоиспользуемые библиотеки, такие как stl или boost.


Я знаю, что это довольно долго, но я думаю, что я охватил все темы, которые вы предложили, и даже может быть возможно выбрать комбинации из предложенных вами, но каждая из них имеетих компромисс, поскольку всегда есть плюсы и минусы для конкретных вещей, которые вы делаете.Увеличьте скорость работы контейнера и посмотрите на стоимость большего объема памяти и потерю прямого доступа, в отличие от меньшего объема используемой памяти, быстрый поиск, но гораздо более медленную вставку.

Знание того, когда и где использовать, какие типы контейнеров и соответствующий алгоритм является ключом к наличию приложения, которое работает эффективно и с минимальными ошибками.

Надеюсь;это может вам помочь.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...