Производительность кэша векторов, матриц и кватернионов

#c #c #arrays #optimization #data-structures

#c #c #массивы #оптимизация #структуры данных

Вопрос:

В прошлом я неоднократно замечал, что код на C и C использует следующий формат для этих структур:

 class Vector3
{
    float components[3];
    //etc.
}

class Matrix4x4
{
    float components[16];
    //etc.
}

class Quaternion
{
    float components[4];
    //etc.
}
  

Мой вопрос в том, приведет ли это к повышению производительности кэша, чем, скажем, это:

 class Quaternion
{
    float x;
    float y;
    float z;
    //etc.
}
  

… Поскольку я бы предположил, что члены класса и функции в любом случае находятся в непрерывном пространстве памяти? В настоящее время я использую последнюю форму, потому что нахожу ее более удобной (однако я также вижу практический смысл в форме массива, поскольку она позволяет обрабатывать оси как произвольные, зависящие от выполняемой операции).


После того, как я воспользовался некоторыми советами респондентов, я протестировал разницу, и на самом деле с массивом это происходит медленнее — я получаю разницу в частоте кадров примерно на 3%. Я реализовал operator[] для переноса доступа к массиву внутри Vector3. Не уверен, имеет ли это какое-либо отношение к этому, но я сомневаюсь в этом, поскольку это все равно должно быть встроено. Единственным фактором, который я мог видеть, было то, что я больше не мог использовать список инициализаторов конструктора на Vector3(x, y, z) . Однако, когда я взял исходную версию и изменил ее, чтобы больше не использовать списки инициализатора конструктора, она работала очень незначительно медленнее, чем раньше (менее 0,05%). Понятия не имею, но, по крайней мере, теперь я знаю, что первоначальный подход был быстрее.

Комментарии:

1. Почему бы просто не определить средства доступа? ( vec.x() , vec.y() , …)

2. @MatthieuM. Это именно то, что я только что закончил делать. 😉

Ответ №1:

Эти объявления не эквивалентны в отношении расположения памяти.

 class Quaternion
{
    float components[4];
    //etc.
}
  

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

Приведет ли это к улучшению или ухудшению производительности, зависит от вашего компилятора, поэтому вам придется его профилировать.

Комментарии:

1. Компилятор может свободно добавлять дополнения также в C или только в C ?

2. Не могли бы вы привести конкретный практический пример, где в последнем случае было бы вставлено дополнение, но не в первом? Или это чисто теоретический аргумент?

3. @aix: Я думаю, с float этого, вероятно, никогда бы не было. В общем, на некоторых платформах доступ к памяти намного быстрее, если адреса выровнены определенным образом (например, начинаются с кратного 4).

4. @amit: Я не знаю, но я думаю, что да, потому что приведенное выше объявление также является модулем, а модули гарантированно будут иметь такое же расположение памяти, какое они имели бы в C.

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

Ответ №2:

Я полагаю, что разница в производительности от подобной оптимизации минимальна. Я бы сказал, что нечто подобное приводит к преждевременной оптимизации для большей части кода. Однако, если вы планируете выполнять векторную обработку поверх своих структур, скажем, с помощью CUDA, композиция структуры имеет важное значение. Посмотрите на стр. 23, если это интересно:http://www.eecis.udel.edu /~mpellegr/eleg662-09s/li.pdf

Комментарии:

1. Я поддержал этот ответ, основанный на полезной информации CUDA (спасибо), но я должен сказать, что «минимальная разница в производительности» здесь означает «все или ничего». Даже один лишний байт между элементами будет оказывать относительно большое влияние на производительность кэша.

2. @NickWiggill Я бы сказал, что самый простой способ — это проверить это, написать два простых тестовых примера и опробовать его. Не хочу говорить, что вы неправы, вы, безусловно, можете быть правы, но опять же, для меня это звучит как преждевременная оптимизация. Предположение о том, как код может повлиять на производительность кэша, различается в разных архитектурах, и даже с одной архитектурой это трудно понять без эмпирического тестирования.

3. @NickWiggill Поздравляет с ускорением. 3% — это не так уж много, но каждая мелочь имеет значение, когда производительность имеет решающее значение. Однако я бы рекомендовал отнестись к этому с недоверием. Подобные микрооптимизации часто не стоят того, чтобы их выполнять — т. Е. вы можете не получить одинаковую производительность на разных платформах или компилятор может фактически выполнить эту оптимизацию с включенными разными флагами. Кэш — сложная вещь в освоении. Я слышал много историй о людях, утверждающих, что их производительность выше, когда они делают один микроопт, чтобы фактически снизить производительность на другой платформе, вот почему микроопты часто выполняются динамически.

4. На самом деле, я имел в виду, что оригинальный метод на 3% быстрее, а не новый (массивы). Повторяю ваш комментарий по поводу микрооптимизации, да, я нахожу, что это очень непостоянная вещь, о которой не стоит беспокоиться, особенно на этих ранних стадиях, когда все существенно изменится, и большинство, если не все эти микропроцессоры, все равно будут потеряны. Решающее значение имеет то, что более широкие алгоритмы обладают высокой производительностью. Но я думаю, что хорошее понимание производительности кэша является довольно ключевым, поскольку последствия того, что я прочитал, могут быть значительными.

5. @NickWigill Это может быть большим. Но, как я уже сказал, производительность кэша на таком маленьком уровне трудно точно предсказать, и практически невозможно предсказать кроссплатформенность. Вот почему сейчас проводится множество передовых исследований по оптимизации компилятора, посвященных динамическому выполнению микроопт методом проб и ошибок для каждой машины.

Ответ №3:

Я не уверен, что компилятору удается лучше оптимизировать код при использовании массива в этом контексте (подумайте, например, о объединениях), но при использовании API, таких как OpenGL, это может быть оптимизацией при вызове таких функций, как

 void glVertex3fv(const GLfloat* v);
  

вместо вызова

 void glVertex3f(GLfloat x, GLfloat y, GLfloat z);
  

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

Комментарии:

1. Вы все равно не будете вызывать эти функции, если вас действительно волнует производительность (копирование 3-х значений с плавающей запятой — ваша наименьшая проблема). И даже тогда, возможно, неопределенный, но практически без проблем, вы все равно можете использовать glVertex3fv(amp;v.x) .