Програмиране на Visual C, брой #25, 26 ноември 2000 г
Поздрави, скъпи абонати!
СТАТИЯПрофилиране: анализ и оптимизация
Профилирането е много мощен инструмент за анализиране на поведението на вашата програма, което ви позволява да идентифицирате "тесните места", където възникват спадове в производителността по време на нейното изпълнение. С помощта на профилиране можете да получите статистика за повикване на функция, за да използвате тази информация по-нататък, когато оптимизирате кода си. В крайна сметка всеки разбира, че няма смисъл да се оптимизира функция, която се извиква само няколко пъти. Когато използвате MFC, това е още по-уместно, защото. много обаждания не винаги са очевидни за програмиста.
Друга причина за използването на профилиране е контролът върху качеството на тестовете. Такъв контрол предоставя информация дали даден тест извиква и обработва резултатите от изпълнението на всички подпрограми без изключение и кои редове код се изпълняват.
Профилирането по същество изобщо НЕ е аналогично на отстраняването на грешки и се използва с различна цел: не за улавяне на грешки, а за подобряване на работата на приложението - в по-голямата част от случаите това е еквивалентно на "за увеличаване на скоростта на работа".
И така, профилирането се използва за определяне на:
1. Използваният алгоритъм оптимален ли е (от гледна точка на времето);
2. Твърде много (или твърде малко) извиквания на подпрограми;
3. Дали частта от кода е покрита от тестови процедури.
Има два вида профилиране:по функцииипо редове от код.
Профилирането на функциятае полезно за идентифициране на неефективен код. Също така е по-бързо от профилирането ред по ред, напр събират по-малко информация. Включва следните измервания:
• общо време впо време на което функцията е била изпълнена + броя на повикванията към тази функция (време на функцията),
• само броя на извикванията на функциите (броене на функции),
• списък с никога неизвиквани функции (покритие на функцията),
• запис на съдържанието на стека при всяко извикване на функция (приписване на функция).
Профилирането ред по редсе използва за тестване на алгоритми, защото ви позволява да видите колко пъти всеки ред е бил изпълнен, както и да идентифицирате редове, които изобщо не са били изпълнени. Тук има само две опции: броене на редове - т.е. колко пъти е изпълнен дадения ред; и покритие на линия - показва онези линии, които са били изпълнени поне веднъж.
Да преминем към практиката. Ако сте инсталирали Visual C++ Professional или Enterprise Edition, тогава имате профилиращ вграден в IDE. Остава само да се научите как да го използвате. Предвиждайки потока от писма, отбелязвам, че има доста голям избор от всякакви профайлери от трети страни, чиито възможности понякога са наистина впечатляващи. Но в тази статия ще разгледам набързо възможностите за профилиране, вградени във Visual C++.
На първо място, трябва да зададете опциите на проекта, за да активирате профилиране (т.е. генериране на информация за профилиране). Това става чрез Project SettingsLinkEnable Profiling.
След това изберете BuildProfile и ще се появи диалоговият прозорец "Profile", където можете да изберете всеки тип профилиране, плюс има възможност да персонализирате всичко това с помощта на Custom Options (вижте параметрите на командата PREP). Опцията Обединяване също е забележителна - тя ви позволява да комбинирате текущите резултати с предишните за визуално сравнение. След щракване върху "ОК" вашата програма стартира - след това извършвате действията, които са ви необходимипроверка. След завършване на вашата кандидатура информацията за профилиране се показва в прозореца за извеждане на профила, където я анализирате ... и правите заключения.
И накрая, няколко съвета.
1. Не трябва да профилирате цялото приложение, по-добре е да се концентрирате върху някои отделни части, които представляват най-голям интерес. (Вижте опциите /EXC и /INC). Има много части, които просто няма смисъл да се профилират - като потребителския интерфейс, например.
2. Измерванията не винаги ще бъдат точни, така че има смисъл да се правят средно няколко преминавания. Можете да събирате статистика за няколко преминавания, като използвате опцията Обединяване.
3. Когато профилирате, опитайте се да запазите броя на изпълняваните процеси в системата до минимум.
4. По-добре е да прекъснете връзката с локалната мрежа или интернет, за да не се налага операционната система да приема входящи пакети.
5. Следете броя на обажданията. Например, ако имате итерация от хиляда повторения в алгоритъм - уверете се, че функциите, които използва, се извикват съответния брой пъти.
6. Във времето, през което се изпълнява функцията, се включва и времето, през което са били изпълнени всички функции, извикани от тази.
За тези, които се интересуват от тази тема, предлагам следните статии в MSDN:
• Използване на Profile, PREP и PLIST
• Профилиране от средата за разработка
В.Имам въпрос към гуруто. На конференции възникна няколко пъти, но някак тихо приключи. Дали това е очевидна истина, или никой не знае (в което не вярвам). Така че въпросът е: как да изберете меню от десния бутон в програмата, същото като в Explorer? Как да си натъпча елементите там? Вторият втори въпрос изчезва, ако е възможно да извадите менюто, а не някаква системна функция,който показва прозорец на менюто и трябва да се примирите с него, както и с факта, че сте този екранен елемент ...
A1 Създаването на контекстно меню (с десен бутон) е доста просто:
2. За най-левия елемент от най-високо ниво въведете име на нишка и добавете команди към полученото падащо меню.
3. Вмъкваме манипулатора на съобщения WM_CONTEXTMENU в класа "view" или в класа на друг прозорец, който получава съобщения от бутоните на мишката, например в CMyDialog. Ние програмираме този манипулатор по следния начин:
CMyDialog::OnContextMenu(CWnd* pWnd, CPoint point)
menu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN TPM_RIGHTBUTTON, point.x, point.y, this);
Това е всичко. TrackPopupMenu и отговаря за показването на контекстното меню на екрана. Вярно е, че е по-добре да направите обект от класа CMenu член на класа, тогава във всяка функция ще бъде възможно да изтриете, добавите, деактивирате и т.н. елементи от менюто. Разбира се, в този случай m_Menu.LoadMenu(IDR_MYMENU); необходимо е да се напише в OnInitDialog. Имайте предвид, че OnContextMenu получава координатите на курсора, т.е. можем просто да покажем различни менюта за различни области на прозореца, просто като проверим координатите.
Всеки обект на обвивка трябва да реализира COM интерфейса IShellFolder. Много обекти също така реализират редица други интерфейси. И така, IExtractIcon е отговорен за иконата на обект, а IContextMenu е отговорен за неговото контекстно меню. Explorer използва тези (и други) интерфейси, за да покаже правилно членовете на йерархията на обекти и да позволи на потребителя да ги манипулира. Ние също можем да използваме тези интерфейси.
И така, имаме нужда от функция, която да показва контекстно меню за даден файл (директория), което да му се предава като параметър. Тази функция трябва да прави следното:
• Вземете интерфейса IContextMenu затози файл (директория).
• Създайте изскачащо меню (чрез CreatePopupMenu).
• Попълнете го с елементи, като използвате IContextMenu::QueryContextMenu.
• Показване на менюто на потребителя (TrackPopupMenu).
• Изпълнете избраната команда чрез IContextMenu::InvokeCommand.
Основната трудност всъщност е първият етап. Можем да получим указател към IContextMenu само ако имаме указател към основния интерфейс IShellFolder, но Windows не предоставя лесен начин за получаване на този указател. Изпълнението на тази задача от своя страна е разделено на няколко стъпки:
– Вземете интерфейса на работния плот IShellFolder чрез SHGetDesktopFolder.
– Създайте LPITEMIDLIST за даден файл (директория) с помощта на IShellFolder::ParseDisplayName.
– Вземете IShellFolder за този файл, като извикате IShellFolder::BindToObject.
void ShowContextMenu(CWnd *pWnd, LPCTSTR pszPath, CPoint point)
// Изградете напълно квалифицираното име.
GetFullPathName(pszPath, sizeof(tchPath)/sizeof(TCHAR), tchPath, NULL);
// Ако е необходимо, конвертирайте ANSI в UNICODE.
if(IsTextUnicode (tchPath, lstrlen (tchPath), NULL)) lstrcpy ((char *)wchPath, tchPath);
иначе MultiByteToWideChar(CP_ACP, 0, pszPath, -1, wchPath, sizeof(wchPath)/sizeof(WCHAR));
// Вземете интерфейса на работния плот IShellFolder
// Преобразуване на пътя към LPITEMIDLIST
pDesktopFolder->ParseDisplayName(pWnd->m_hWnd, NULL, wchPath, NULL, &pidl, NULL);
// Вземете интерфейса IShellFolder за дадения файл (папка)
pDesktopFolder->BindToObject(pidl, NULL, IID_IShellFolder, (void**)&pFolder);
// Вземете интерфейса IContextMenu за дадения файл (папка)
pContextMenu->QueryContextMenu(PopupMenu.m_hMenu, 0, 1, 0x7FFF,CMF_EXPLORE);
UINT nCmd = PopupMenu.TrackPopupMenu(TPM_LEFTALIGNTPM_LEFTBUTTONTPM_RIGHTBUTTONTPM_RETURNCMD, point.x, point.y, pWnd);
// Изпълнение на командата (ако е избрана)
// Вземете интерфейса IMalloc.
// Използвайте го, за да освободите памет, разпределена за ITEMIDLIST
// Освобождаване на всички получени интерфейси
Тази функция може да бъде извикана например от манипулатора OnContextMenu. Прави се така:
void CMyView::OnContextMenu(CWnd* pWnd, CPoint point)
ShowContextMenu(pWnd, "C:\\command.com", точка);
– Периодични издания 1997 г., Microsoft Systems Journal, април, Wicked Code
– Описание на IShellFolder и IContextMenu
Що се отнася до втория въпрос (относно създаването на наши собствени елементи от менюто), ние имаме пълен контрол върху процеса на създаване на менюто, което означава, че можем да правим каквото си поискаме с него. Просто трябва да имате предвид 2 точки.
Първо, тъй като функцията TrackPopupMenu се извиква с флага TPM_RETURNCMD, тя няма да изпрати съобщение WM_COMMAND до прозореца. Следователно трябва да анализирате стойността nCmd, върната от функцията TrackPopupMenu, и да извикате желания манипулатор ръчно. Например:
UINT nCmd = PopupMenu.TrackPopupMenu(…);
ако (nCmd == 0x8000)
Второ, функцията IContextMenu::QueryContextMenu получава параметрите idCmdFirst, idCmdLast (в примера по-горе те са равни съответно на 1 и 0x7FFF). Идентификаторите за стандартни елементи от менюто се избират точно в диапазона от idCmdFirst до idCmdLast. Следователно трябва да се уверите, че идентификаторите на персонализирани елементи от менюто не попадат в този диапазон.
(Александър Шаргин)ОБРАТНА ВРЪЗКА
Просто трябва да създадете манипулатор на събития WM_ERASEBKGND с такъведна линия:
BOOL CSomeClass::OnEraseBkgnd(CDC* pDC)
Тоест, казано на български, програмата не изчисти фона, рисува напълно.
Към отговора на А1 от миналия брой:
Сега тренирайте. Нека има готово SDI приложение (с Document/View технология). Създаваме допълнителен изглед. Това се прави във функцията CFrameWnd::OnCreateClient по следния начин:
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
// клас CNewView е нашият нов изглед
// вземете под внимание идентификатора на новия изглед
// променливата m_pNewView е декларирана в CMainFrame като
m_pNewView = STATIC_DOWNCAST(CNewView, CreateView(pContext, AFX_IDW_PANE_FIRST+1));
m_pNewView->ShowWindow(SW_HIDE); // за нулиране на флага WS_VISIBLE
връщане на CFrameWnd::OnCreateClient(lpcs, pContext);
Този код не работи правилно и се вижда дори с просто око. В последния ред на функцията CMainFrame::OnCreateClient се извиква функцията на базовия клас. Но полето pContext->pNewViewClass вече е променено! В резултат на това вместо два различни изгледа ще бъдат създадени два еднакви. Грешката се коригира чрез преместване на извикването на функцията от базовия клас в началото на заменената функция:
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
int nResult = CFrameWnd::OnCreateClient(lpcs, pContext);
Освен това не е ясно как да използвате SwitchView. Указателят към изгледа, който създадохме, се съхранява в m_pNewView, но изглежда няма удобен начин да получите указател към изгледа, създаден от самия MFC. Вероятно най-добрият вариант е също да го съхранявате в член на класа CMainFrame.
(Александър Шаргин)В ТЪРСЕНЕ НА ИСТИНАТА
В.Имам диалог-базовото приложение живее в системната лента. Необходимо е приложението при рестартиране да намери вече работещ екземпляр на програмата и да го активира. Опитах се да направя това чрез FindWindow(), на който се предава името на регистрирания клас прозорец и заглавието на прозореца, който се търси. Не мога да търся по заглавие, защото се променя през цялото време. Следователно трябва да търсите по името на регистрирания клас прозорец. Тук започва проблемът. Аз не го познавам. Самият MFC ги разпространява към приложения, базирани на диалог. И би било възможно да се замени това име в PreCreateWindow(), но този метод CDialog не наследява от CWnd. Във всички останали методи името на класа вече е регистрирано, т.е. твърде късно е да го промените. Как да бъдем?