пятница, 10 декабря 2010 г.

Paged Geometry или почему я не люблю STL streams, а также как их правильно готовить.

Небольшое вступление, не совсем в тему, зато весьма поучительное и отражающее основную суть вопроса.

File copy circa 1975:

#include <stdio.h>
int main() {
int c;
while ((c = getchar()) != EOF)
    putchar(c);
    return errno != 0;
}

Fast forward 20 years, and...

#include <iostream>
#include <algorithm>
#include <iterator>
#include <string>
using namespace std;
int main() {
    // (forgot the try/catch around main)
    copy(istream_iterator<string>(cin),
    istream_iterator<string>(),
    ostream_iterator<string>(cout,"\n"));
}
Something, somewhere, went terribly wrong.

Andrei Alexandrescu.

Я помню, как еще шесть с половиной лет назад, начиная изучать C++ я читал о потоках в STL и думал "Как это круто! Они так неожиданно и здорово перегрузили оператор <<". Однако время идет, а ореола очарования постепенно сошел на нет.

Первая проблема. They are too verbose! (В них чересчур много буков).
Пример из реально работающего кода.

        std::cout << std::setw(2) << std::setfill('0') << (pTime->tm_mon + 1)
            << std::setw(2) << std::setfill('0') << pTime->tm_mday
            << std::setw(2) << std::setfill('0') << (pTime->tm_year + 1900)
            << "_" << std::setw(2) << std::setfill('0') << pTime->tm_hour
            << std::setw(2) << std::setfill('0') << pTime->tm_min
            << std::setw(2) << std::setfill('0') << pTime->tm_sec
            << std::setw(3) << std::setfill('0') << (mTimer->getMilliseconds() % 1000);

В C-стиле это будет выглядеть как
printf( "%02d%02d%02d_%02d%02d%02d%03d", 
        (pTime->tm_mon + 1),
        pTime->tm_mday,
        (pTime->tm_year + 1900),
        pTime->tm_hour,
        pTime->tm_min,
        pTime->tm_sec,
        (mTimer->getMilliseconds() % 1000 
);

Как по мне, так второй код читается лучше, а пишется короче. Так почему же "something has gone wrong?"

Вторая причина. Работаем мы с open-source движком Ogre3D, таким большим, толстым с очень длинными именами классов навроде Ogre::HardwareVertexBufferSharedPtr и достаточно интенсивным использованием строк.


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

Включаю профайлер и что же я вижу? (Обратите внимание на Forests::BatchedGeometry::SubBatch::build/)

Функция которая должна вызываться не чаще, чем раз в 10 кадров пятая по времени среди "горячих точек"!

Копаем дальше! Это результаты еще одной сессии профилирования, где BatchedGeometry::build заняло в целом около 1.4 с. времени.


Ничего себе. В коде генерации деревьев больше половины времени занимают операции со строками!

Как только код c
String BatchedGeometry::getFormatString(SubEntity *ent)
{
 StringUtil::StrStreamType str;
 
 str << ent->getMaterialName() << "|";
 str << ent->getSubMesh()->indexData->indexBuffer->getType() << "|";
 
 const VertexDeclaration::VertexElementList &elemList = ent->getSubMesh()->vertexData->vertexDeclaration->getElements();
 VertexDeclaration::VertexElementList::const_iterator i;
 for (i = elemList.begin(); i != elemList.end(); ++i)
 {
  const VertexElement &element = *i;
  str << element.getSource() << "|";
  str << element.getSemantic() << "|";
  str << element.getType() << "|";
 }
 
 return str.str();
}

был заменен на

String BatchedGeometry::getFormatString(SubEntity *ent)
{
   const int BufSize = 1024;
   char buf[BufSize];


   int countWritten = 
         _snprintf_s( buf, BufSize,
               "%s|%d",
                ent->getMaterialName().c_str(), 
                ent->getSubMesh()->indexData->indexBuffer->getType() 
         );

   const VertexDeclaration::VertexElementList &elemList = ent->getSubMesh()->vertexData->vertexDeclaration->getElements();
   VertexDeclaration::VertexElementList::const_iterator i;
   for (i = elemList.begin(); i != elemList.end(); ++i)
   {
      const VertexElement &element = *i;
      countWritten += _snprintf_s( buf + countWritten, BufSize - countWritten, BufSize - countWritten,
                         "|%d|%d|%d",
                          element.getSource(),
                          element.getSemantic(),
                          element.getType()
                        );
   }

   return buf;
}

Время генерации деревьев сократилась в 2-2.2(!!!!!!) раза: вместо средних 4.8-5.6 мс на один вызов стало 2.2-2.8. Не знаю, ругаться на разработчиков PagedGeometry или на реализацию стримов у Майкрософта?

Такая низкая скорость работы объясняется тем, что

  1. сами потоки - объекты, которые любят выделять память, в частности на создании и удалении;
  2. реализация STL от майкрософта по умолчанию вставляет локи на работу с потоками, если проект собирается, как multithreaded.


Итак, они медленные.

Так что я буду по-прежнему использовать printf или класс StringFormatter и пусть меня убеждают  и дальше о пользе некоторых абстракций STL. :)

P.S. Между прочим у Александреску есть отличная статья на тему типо-безопасного printf-а!


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

Отправить комментарий