Блин. Тестил в дебаге))
Перевел в релиз с -O2
dirty прямая
0.203168
0.201715
0.206481
без dirty прямая
0.516027
0.514201
0.512298
MrShoor
Я уже думаю как организовать классы для такого рендеринга. Детально с ними не разбирался, так что это скорее всего надолго.
Update.
Не там убирал проверку. Разница двукратная.
Dampire
> Короче что с dirty, что без разница незначительная на самом деле.
Твой тест не совсем корректен. У тебя по дефолту dirty сброшен, так что ты всегда считаешь
Вернее не так. 1 цикл ты считаешь всегда, а остальные 2 ты просто скипаешь если флаг dirty.
Я же говорю на таком тесте ты не проверишь. И как я сказал у тебя тяжелый апдейт.
1. Тебе лучше иметь два флага 1 для мировой матрицы, 2й для инверсной.
2. Инверсную считать только по запросу.
const glm::mat4 & Transform::GetMatrix() { if( dirtyInverse) { inverse = matrix.Inverse( ); } return inverse; }
Эта функция будет вызываться только в случае если тебе действительно нужна инверсная матрица.
3. Если посмотреть внимательно на твой код:
matrix = glm::translate(glm::mat4( 1.f), position); // glm::vec3 position matrix = matrix * glm::toMat4( rotation); // glm::quaternion rotation matrix = glm::scale( matrix, scale); // glm::vec3 scale matrixInverse = glm::inverse( matrix); if( parent) { matrix = parent->GetWorldMatrix( ) * matrix; matrixInverse = parent->GetWorldMatrixInv( ) * matrixInverse; }
первые 3 строчки тебе вообще не нужны. Передавай в объект локальную матрицу а все трансформации с ней делай снаружи.
matrixInverse = glm::inverse(matrix);
тоже не нужна поскольку операция очень дорогая, а эта матрица нужна не так часто.
if(parent)
{
matrix = parent->GetWorldMatrix() * matrix;
matrixInverse = parent->GetWorldMatrixInv() * matrixInverse;
}
Собственно у тебя тут останется только matrix = parent->GetWorldMatrix() * matrix;
В данном случае лучше хранить две матрицы:
local = это собственно локальная матрица объекта
world = parent->matrix * matrix;
для корневого объекта local == world
Стас
Ну? А в реальной ситуации как? Что не так то?
dirty/без dirty означает что проверка на условие запилена или выпилена соответственно, а не то, что, я предполагаю, ты подумал. Когда проверка на dirty присутствует оно быстрее в 2.5 - 3 раза.
Dampire
У тебя неправильный тест. Вот, смотри:
Выводит:
Test_OO_no_dirty: 1.13998 Test_OO_dirty: 0.629987 Test_DO_dirty: 0.249995 Test_DO_no_dirty: 0.269995
Какие из этого можно сделать выводы?
1. Data driven в 2 раза эффективнее, чем object-oriended с dirty флагом
2. Test_DO_dirty просчитывает в 2 раза _меньше_ матриц, чем Test_DO_no_dirty, при этом по времени выполнения они почти не отличаются. То есть стоимость кэш-мисса == 2 апдейта матриц.
Где кешмисс?
Test_DO_dirty* item = &data[i + j]; if (!item->dirty) { item->dirty = !item->dirty; continue; } if ( item->dirty) { item->matrix = XMMatrixIdentity( ); item->matrix = XMMatrixMultiply( item->matrix, item->matrix); item->matrix = XMMatrixMultiply( item->matrix, item->matrix); item->matrix = XMMatrixMultiply( item->matrix, item->matrix); }
Вот тут кешмисс - проверка на dirty флаг и ранний выход из цикла. Как видишь, нифига это не оптимизация :)
Ну и нужно также помнить, что это очень синтетический пример и в реальном мире кешмисс будет намного дороже.
Да бесполезно это обьяснять, когда чел размахивает пейпером как флагом... Хотел я написать сразу же, да в лом было. Там прям какая то идеальная ситуайия) и обьекты выровнены, и transform занимает 12 тактов, ппичем код трасформа не указан, красота. 12 тактов, жто допустим 12 сложений, при условии что данныкюе УЖЕ в регистрах, а их то надо еще грузить и чтение со второго кеша ну нифига не однотактовое. Да и лишний jmp к функции transform стоит тактов.
Zloten
Я вот привёл тест. Что в нём не так?
bazhenovc
Не понял, а зачем два раза проверять флаг?
bazhenovc
> Ну и нужно также помнить, что это очень синтетический пример и в реальном мире
> кешмисс будет намного дороже.
Ну пример и правда уж очень синтетический. Ты просто показываешь вычисление матрицы. А у него в апдейте как минимум штук 5 умножений + инверсия... так что в его случае dirty флаг как раз дает прирост,
И то что ты показываешь крайне сложно показать синтетическими тестами
Zloten
Вторая проверка не вызывает кешмисс и ни на что не влияет. Я его убрал - результаты не изменились.
Test_DO_dirty* item = &data[i + j]; if (!item->dirty) { item->dirty = !item->dirty; continue; } item->matrix = XMMatrixIdentity( ); item->matrix = XMMatrixMultiply( item->matrix, item->matrix); item->matrix = XMMatrixMultiply( item->matrix, item->matrix); item->matrix = XMMatrixMultiply( item->matrix, item->matrix);
bazhenovc
Это неправильные пчелы и они делают неправильный мед. Я понял твою позицию. И я сделаю с проверкой флага, просто потому-что в моем коде оно работает быстрее. Когда перестанет - уберу.
Хм, чтение item->dirty, здесь ту уже по любому получаешь кеш-мисс, ту УЖЕ залез в обьект. Поэтому и разницы нет.
Стас
> Ну пример и правда уж очень синтетический. Ты просто показываешь вычисление
> матрицы. А у него в апдейте как минимум штук 5 умножений + инверсия...
Применение data driven подхода будет давать прирост перформанса в любом случае, независимо от количества умножений.
С dirty флагом сложнее - он действительно не всегда выгоден, поэтому нужно профилировать и смотреть следующие вещи:
1. Будет ли dirty флаг дороже, чем тупо посчитать лишнее?
2. Если dirty флаг даёт серьёзный прирост, может попробовать переписать/оптимизировать математику?
3. Окупается ли выигрыш по перформансу, достигнутый такими оптимизациями? (имеется в виду затраченное программерское время на написание и будущую поддержку этого кода)
Zloten
> Хм, чтение item->dirty, здесь ту уже по любому получаешь кеш-мисс, ту УЖЕ залез
> в обьект. Поэтому и разницы нет.
Нет, объект уже находится в кеше, потому что я его туда префетчил. Кешмисс возникает на следующей итерации цикла, когда в кеше внезапно оказывается не тот объект.
Стас
Я уже переделал под инверсию/прямую. Есть 2 флага и 2 функции апдейта матрицы. Хранить локальную и глобальную я не хочу, потому-что я пока не вижу примеров использования локальной матрицы. Позиция/вращение/размер у меня всегда относительно родителя, матрица же всегда мировая.
А зачем его префетчить? если он ВОЗМОЖНО не понадобиться? Как пример хранить дерти флаг в младшем бите указателя на обьект:
struct UObject { Matrix4 data1; Matrix4 data2; }; void transform( UObject* objects, Matrix4* matrix ) { // do something } void update( UObject** objects, Matrix4* matrix ) { uint32 num = 1000; do { uint32 addr = (uint32) *objects; if ( UNLIKELY( addr & 1 ) ) { transform( *objects = (UObject*) ( addr & ~1 ), matrix ); } objects++; } while ( --num ); }
ЗЫ И если ты заранее префетчишь то все возможные кеш-мисы ты уже словил.