Я пробую Data Oriented Design - могу ли я сделать это с помощью std :: vector? - PullRequest
1 голос
/ 15 февраля 2012

Хорошо, вот пример кода, сравнивающий решение объектно-ориентированного программирования (OOP) с решением Data Oriented Design (DOD) для обновления группы шаров.

const size_t ArraySize = 1000;

class Ball
{
public:
    float x,y,z;
    Ball():
        x(0),
        y(0),
        z(0)
    {
    }

    void Update()
    {
        x += 5;
        y += 5;
        z += 5;
    }
};

std::vector<Ball> g_balls(ArraySize);

class Balls
{
public:
    std::vector<float> x;
    std::vector<float> y;
    std::vector<float> z;

    Balls():
        x(ArraySize,0),
        y(ArraySize,0),
        z(ArraySize,0)
    {
    }

    void Update()
    {
        const size_t num = x.size();
        if(num == 0)
        {
            return;
        }

        const float* lastX = &x[num - 1];

        float* pX = &x[0];
        float* pY = &y[0];
        float* pZ = &z[0];
        for( ; pX <= lastX; ++pX, ++pY, ++pZ)
        {
            *pX += 5;
            *pY += 5;
            *pZ += 5;
        }
    }
};

int main()
{
    Balls balls;

    Timer time1;
    time1.Start();
    balls.Update();
    time1.Stop();

    Timer time2;
    time2.Start();
    const size_t arrSize = g_balls.size();
    if(arrSize > 0)
    {
        const Ball* lastBall = &g_balls[arrSize - 1];
        Ball* pBall = &g_balls[0];
        for( ; pBall <= lastBall; ++pBall)
        {
            pBall->Update();
        }
    }
    time2.Stop();


    printf("Data Oriented design time: %f\n",time1.Get_Microseconds());
    printf("OOB oriented design  time: %f\n",time2.Get_Microseconds());

    return 0;
}

Теперь это компилируется и запускается в Visual Studio, хотя мне интересно, разрешено ли мне это делать, и должно ли это быть в состоянии надежно сделать это:

const float* lastX = &x[num - 1];//remember, x is a std::vector of floats

float* pX = &x[0];//remember, x is a std::vector of floats
float* pY = &y[0];//remember, y is a std::vector of floats
float* pZ = &z[0];//remember, z is a std::vector of floats
for( ; pX <= lastX; ++pX, ++pY, ++pZ)
{
    *pX += 5;
    *pY += 5;
    *pZ += 5;
}

Насколько я понимаю, данные в std :: vector должны быть смежными, хотя я не уверен из-за того, как они хранятся внутри, если это будет проблемой на другой платформе, если это нарушает стандарт.Кроме того, это был единственный способ получить решение DOD, превосходящее решение ООП, любой другой способ итерации был не так хорош.Я мог бы использовать итераторы, хотя я вполне уверен, что он может быть быстрее, чем ООП с включенной оптимизацией, то есть в режиме релиза.

Итак, это хороший способ сделать DOD (лучший способ?), И это законно c ++?

[EDIT] Хорошо, для DOD это плохой пример;x, y, z должны быть упакованы в Vector3.Итак, хотя DOD работал быстрее в отладке, чем ООП, в выпуске это была другая история.Опять же, это плохой пример того, как вы хотите эффективно использовать DOD, хотя он показывает недостатки, если вам нужен одновременный доступ к группе данных.Ключом к правильному использованию DOD является «проектирование данных на основе шаблонов доступа».

Ответы [ 3 ]

4 голосов
/ 15 февраля 2012

Вопрос со всем кодом и тому подобным немного запутан, поэтому давайте попробуем понять, понимаю ли я, что вам действительно нужно:

Насколько я понимаю, данные в std :: vector должны быть смежными

Это так. Стандарт обязывает хранить данные в векторе непрерывно, что означает, что это будет иметь место на всех платформах / компиляторах, которые соответствуют стандарту.

это был единственный способ получить решение DOD, превосходящее решение ООП

Я не знаю, что вы имеете в виду под DOD

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

На самом деле, итераторы в этом случае (при условии, что у вас отключены отладочные итераторы в VS) будут такими же быстрыми, если не быстрее, чем прямые модификации через указатели. Итератор в вектор может быть реализован с простым указателем на элемент. Опять же, обратите внимание, что по умолчанию в VS итераторы выполняют дополнительную работу, помогая отладке.

Следующее, что нужно учитывать, это то, что схема памяти двух подходов отличается, что означает, что если на более позднем этапе вам нужно получить доступ ко всем x, y и z из одного элемента, в В первом случае они, скорее всего, попадут в одну строку кэша, в то время как при подходе трех векторов это потребует извлечения памяти из трех разных мест.

1 голос
/ 06 января 2018

Как уже указывалось, вектор был в целом смежным до C ++ 11 и теперь гарантирован как таковой с помощью нового метода data, который фактически возвращает прямой указатель на используемый им внутренний массив.Вот ваша стандартная цитата ISO C ++:

23.2.6 vector шаблона класса [vector]

[...] Элементы вектора хранятся непрерывно, что означает, что если vвектор, где T - это какой-то тип, отличный от bool, тогда он подчиняется тождеству &v[n] == &v[0] + n для всех 0 <= n < v.size().

Тем не менее, я хотел перейти в основном из-за того, как выВы тестируете и используете "DOD":

Итак, хотя DOD работал быстрее в отладке, чем в ООП, в выпуске это была другая история.

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

Дизайн, ориентированный на данные, - это просто обобщенный подход к проектированию, в котором вы рассматриваете, как хранить и эффективнополучить доступ к данным заранее.Это становится одной из первых вещей, которые необходимо учитывать при подходе к проектам с использованием этого мышления.Противоположность начинается, скажем, с проектирования архитектуры, пытающейся выяснить все функциональные возможности, которые она должна предоставлять, вместе с объектами и абстракциями, а также с чистыми интерфейсами и т. Д., И затем оставляя данные в качестве подробностей реализации, которые будут заполнены позже.DOD начинался с данных как фундаментальной вещи, которую нужно учитывать на стадии проектирования, а не как детали реализации, которую нужно заполнить в качестве запоздалой мысли.Это полезно в критических для производительности случаях, когда производительность является фундаментальным требованием уровня проектирования, требуемым заказчиками, а не просто роскошью реализации.

В некоторых случаях эффективное представление структур данных фактически приводит к новым функциям, в некоторой степенипозволяя сами данные для разработки программного обеспеченияGit является примером такого программного обеспечения, где его функции фактически вращаются вокруг структуры данных набора изменений в некоторой степени, где его эффективность фактически приводит к появлению новых функций.В этих случаях функции программного обеспечения и дизайн конечного пользователя фактически развивается из его эффективности, открывая новые двери, потому что эффективность позволяет делать вещи, скажем, в интерактивном режиме, которые ранее считались слишком дорогими в вычислительном отношении, чтобы делать их в любом разумном количестве.время.Другим примером является ZBrush, который изменил мою индустрию VFX, позволив людям сделать вещи, которые люди считали невозможными пару десятилетий назад, например, интерактивно лепить 20 миллионов полигональных сеток с помощью скульптурной кисти для создания моделей, столь детализированных, каких еще никто не видел в конце 90-хи в начале 2000-х годов.Другой способ - это трассировка воксельных конусов, которая позволяет играм, даже написанным на Playstation, иметь непрямое освещение с рассеянным отражением;что-то, что люди все еще думают, что для рендеринга одного кадра без таких ориентированных на данные методов требуется минуты или часы, а не 60+ кадров в секунду.Поэтому иногда эффективный подход DOD на самом деле дает новые функции в программном обеспечении, которые раньше считали невозможными, потому что он преодолевает аналоговый звуковой барьер.

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

Кроме того, это только мое мнение об этом, но, по моему мнению, DOD не должен заранее добиваться наиболее эффективного представления данных.Для этого нужно заранее разработать наиболее эффективные интерфейсы, чтобы оставить достаточную передышку для оптимизации по мере необходимости.Примером программного обеспечения, которому, по-видимому, не хватает предвидения, которое обеспечило бы мышление DOD, могло бы быть программное обеспечение для композитинга видео, которое представляет такие данные:

class IPixel
{
public:
    virtual ~IPixel() {}
    ...
};

Один лишь взгляд на приведенный выше код может показать, что существует значительный недостаток предвидения в том, как спроектировать вещи для эффективного представления и доступа к данным.Для начала, если учесть 32-битный пиксель RGBA, стоимость виртуального указателя с учетом размера и выравнивания 64-битного указателя будет четырехкратным размером одного пикселя (64-битный vptr + 32-пиксельные данные + 32-битные отступы для выравнивания vptr).Поэтому любой, кто применяет мышление DOD, обычно избегает таких конструкций интерфейса, как чума.Тем не менее, они могут получить выгоду от абстракции, например, возможность использовать один и тот же код для изображений с различными форматами пикселей.Но в этом случае я бы ожидал:

class IImage
{
public:
   virtual ~IImage() {}
   ...
};

..., который упрощает накладные расходы vptr, виртуальную диспетчеризацию, возможную потерю смежности и т. Д. До уровня всего изображения (возможно, миллионов)пикселей), а не что-то оплачиваемое за пиксель.

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

enter image description here

Потому что теперь, скажем, выхочу многопоточность обработки множества шаров одновременно везде.Вы не можете без переписывания всей кодовой базы, используя шары по отдельности.В качестве другого примера допустим, что вы хотите изменить представление шара с AoS на SoA.Для этого потребуется переписать Ball, чтобы он стал Balls вместе со всей кодовой базой, используя прежний дизайн Ball.Аналогичная вещь, если вы хотите обрабатывать шары на GPU.Поэтому, как правило, мышление DOD предпочитает более грубый дизайн, такой как Balls:

enter image description here

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

class Balls
{
public:
    ...
private:
    struct Ball
    {
        ...
    };
    vector<Ball> balls;
};

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

Наконец, для вашего теста, что делаетэто делать?По сути, он проходит по цепочке поплавков одинарной точности и добавляет к ним 5.В этом случае не имеет значения, сохраняете ли вы один массив чисел с плавающей точкой или тысячу.Если вы храните больше массивов, то это неизбежно приводит к дополнительным издержкам, которые не приносят никакой пользы, если все, что вы собираетесь делать, - это циклически обходить все числа с плавающей запятой и добавлять к ним 5.

Чтобы использовать представление SoA,Вы не можете просто написать код, который делает одно и то же для всех полей.SoAs обычно преуспевают в последовательном доступе к нетривиальным размерам ввода, когда вам действительно нужно сделать что-то другое с каждым полем, например преобразовать каждое поле данных x / y / z, используя матрицу преобразования с эффективными инструкциями SIMD (написанными от руки или сгенерированнымиваш оптимизатор) превращая 4+ шарика одновременно, а не просто добавляя 5 к куче поплавков.Они особенно хороши, когда не все поля горячие, как физическая система, которая не интересуется спрайтовым полем частицы (которое было бы расточительно загружать в строку кэша только для того, чтобы не использовать ее).Таким образом, чтобы проверить различия между представителем SoA и AoS, вам нужен достаточно реальный эталонный тест, чтобы увидеть практические различия.

1 голос
/ 15 февраля 2012

Да, вы можете сделать это.

Векторные контейнеры реализованы в виде динамических массивов;Как и обычные массивы, векторные контейнеры хранят свои элементы в смежных местах хранения, что означает, что к их элементам можно обращаться не только с помощью итераторов, но также с помощью с использованием смещений на обычных указателях на элементы .http://cplusplus.com/reference/stl/vector/

...