22 ноября 2006

Build optimization

Решил поискать пути ускорения build-а не меняя аппаратную часть компьютера. Перенес includes из boost на RAM disk. Проект который раньше компилировался пять минут теперь компилируется четыре.

Убил RAM disk, перенес весь используемый boost и стандартную библиотеку в precompiled header. Компиляция того проекта укладывается уже в 2 минуты.

21 ноября 2006

Smart pointers rule

Набрел в сети на Process Explorer, побаловался с ним. Посмотрел сколько мое приложение ест памяти и т.п. Провел эксперимент - в приложении несколько раз открыл-закрыл довольно тяжелое по требованиям к памяти окно.

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

Но, спрашивается, если я везде использую умные указатели, то какого хрена получаю утечки памяти? Это напрягло меня вдвойне. Я никогда в жизни не искал утечки памяти - на C++ пишу чуть больше года, до этого писал на VBA с его COM-объектами и отсутствием заморочек на счет освобождения памяти.

Однажды я попробовал некую небольшую утилиту для поиска памяти - она встраивалась в Visual Studio и по завершении приложения выдавала список утечек. Так она мне их выдало такое немерянное количество в моем приложении, что я в это не поверил и снес ее нафиг.

После этого я зарекся при поиске утечек обойтись без утилит, а просто прописать в настройках проекта Force Includes некий хедер-файл, заменяющий malloc и free на их отладочные версии запоминающие кто сколько выделил и освободил ли потом.

Но сейчас мне совсем не хотелось с этим мучаться - я ведь просто игрался с Process Explorer. Но утечки покоя мне не давали.

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

Это был следующий код:
std::wstring CXMLNode::OptionalAttr
(
const std::wstring& AttrName,
const std::wstring& DefaultValue
) const
{
assert( !IsNull() );
const xmlChar* const AttrStr =
xmlGetProp( _Node, WideStringToXMLChars( AttrName ).c_str() );
if ( AttrStr != NULL )
{
return XMLCharsToWideString( AttrStr );
}
else
{
return DefaultValue;
}
}
Это был класс C++-оболочки libxml, написанный не мной, а программистом, работавшим на меня. Я конечно, порадовался, что код не мой ;) И сразу очевидно, утечку дает AttrStr = xmlGetProp(...), так как xmlGetProp() - функция из C-шной библиотеки (libxml), а там про умные указатели не знают.

Так и оказалось - в документации к xmlGetProp значилось:
Returns: the attribute value or NULL if not found. It's up to the caller to free the memory with xmlFree().
А мой программист на xmlFree() забил.

Так как xmlGetProp() постоянно использовалась, пришлось написать для нее "smart pointer", чтобы не напрягать мозги на счет xmlFree(). Что-то типа такого:
// XMLAttr helper for automaticaly memory manage
class CXMLAttr
{
public:
CXMLAttr(xmlNodePtr Node, const std::wstring& AttributeName);
bool IsValid() const;
const std::wstring& Value() const;

private:
bool _IsValid;
std::wstring _Value;
};

inline CXMLAttr::CXMLAttr(xmlNodePtr Node, const std::wstring& AttributeName)
{
std::basic_string Name;
WideStringToXMLChars(AttributeName, Name);

xmlChar* const Attribute = xmlGetProp(Node, Name.c_str());

_IsValid = (Attribute != NULL);

if (Attribute)
{
XMLCharsToWideString(Attribute, _Value);
xmlFree(Attribute);
}
}

inline bool CXMLAttr::IsValid() const
{
return _IsValid;
}

inline const std::wstring& CXMLAttr::Value() const
{
return _Value;
}


Восстановил функциональность приложения, прогнал 2000 открытий-закрытий, перерасхода памяти не обнаружил.

09 ноября 2006

Вам тултипы какой версии?

Тестируя свое приложение под Windows 2000 обнаружил следующий баг: не работают тултипы. В XP работают, а в Win2k - нет.

Окзалось, засада ждала меня в совершенно безобидном операторе:
toolinfo.cbSize = sizeof(TOOLINFO)

Объясню, почему там была засада. Для начала посмотрим как объявлен TOOLINFO (точнее tagTOOLINFOW, т.к. TOOLINFO - его алиас) в commctrl.h:
typedef struct tagTOOLINFOW {
UINT cbSize;
UINT uFlags;
HWND hwnd;
UINT_PTR uId;
RECT rect;
HINSTANCE hinst;
LPWSTR lpszText;
#if (_WIN32_IE >= 0x0300)
LPARAM lParam;
#endif
#if (_WIN32_WINNT >= 0x0501)
void *lpReserved;
#endif
} TTTOOLINFOW, NEAR *PTOOLINFOW, *LPTTTOOLINFOW;

В параметрах компиляции у меня стоит _WIN32_WINNT = 0x0501, чтобы была возможность использовать все возможности WinXP, такие как XP themes и прочие. Как видно выше, при таких настройках я получаю sizeof(TOOLINFO), учитывающее lpReserved, про который Win2000 не знает. И встретив toolinfo.cbSize больше того размера TOOLINFO, которое известно Win2000, функции WinAPI отказываются работать.

Я, однако, ожидал, что WinAPI проглатывает такие структуры когда cbSize > известного текущей версии WinAPI размера структуры, а cbSize нужно наоборот, для того, чтобы более поздние версии WinAPI не пытались читать за границами структур, передаваемых старыми программами.

Ну да ладно, не проглатывает и пусть. Но как быть? Установить _WIN32_WINNT меньше 0x0501 я не могу. Остается "вручную" (вычесть указатель на tooltip из указателя на tooltip.lpReserved) считать размер TOOLINFO для Win2000 и использовать его. Здесь надо заметить, что сам я sizeof(TOOLINFO) не считаю, так как он за меня считается внутри WTL::CToolInfo::Init() ;-) Выход я, конечно найду (откажусь от WTL::CToolTipCtrlT или внесу в WTL::CToolInfo::Init() поправку), но осадок остается.

01 ноября 2006

Unit tests rule

Сегодня на собственной шкуре убедился в ценности юнит-тестов.

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

Но не задел ли я при этом ничего другого?

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

Не будь этого теста, количество ошибок в модуле осталось бы таким как было, просто одна ошибка сменилась на другую ;)

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