Програмування на Visual C Випуск №25 від 26 листопада 2000 р

Вітаю вас, шановні передплатники!

СТАТТЯПрофілювання: аналіз та оптимізація

Профілювання - це дуже потужний інструмент аналізу поведінки вашої програми, що дозволяє під час її виконання виявити "вузькі місця", де відбувається падіння продуктивності. За допомогою профілювання можна отримати статистику виклику функцій, щоб надалі використовувати цю інформацію під час оптимізації коду. Адже кожному зрозуміло, що немає сенсу оптимізувати функцію, що викликається лише кілька разів. З використанням MFC це тим більше актуально, т.к. багато викликів не завжди очевидні для програміста.

Ще одна причина використання профілювання – це контроль якості тестів. Такий контроль надає інформацію про те, чи здійснює даний тест виклик та обробку результатів виконання всіх без винятку підпрограм, та які рядки коду при цьому виконуються.

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

Отже, профіль використовується для того, щоб визначити:

1. Чи оптимальний використаний алгоритм (за часом);

2. Занадто велика (або занадто мала) кількість викликів підпрограми;

3. Чи покривається ділянка коду процедурами, що тестують.

Розділяють два види профілювання:за функціямитаза рядками коду.

Профілювання за функціямикорисно виявлення неефективного коду. Також воно швидше, ніж профільування рядків, т.к. доводиться збирати менше інформації. Включає такі вимірювання:

• сумарний обсяг часу,протягом якого виконувалася функція + кількість викликів цієї функції (function timing),

• лише кількість викликів функції (function counting),

• список функцій (function coverage), що жодного разу не викликалися,

• запис вмісту стека під час кожного виклику функції (function attribution).

Профілювання рядкамивикористовується для перевірки алгоритмів, т.к. дозволяє подивитися, скільки разів було виконано кожен рядок, а також виявити рядки, що не виконалися взагалі жодного разу. Тут лише два варіанти: підрахунок рядків (line counting) – тобто. скільки разів цей рядок був виконаний; та покриття рядків (line coverage) – показує ті рядки, які виконувались хоча б разів.

Перейдемо до практики. Якщо у вас встановлений Visual C++ Professional або Enterprise Edition, то у вас є профільувальник, він вбудований в IDE. Залишилося тільки навчитися користуватися ним. Передбачаючи потік листів, зауважу, що існує досить великий вибір усіляких профільників від сторонніх фірм, можливості яких іноді справді вражають. Але в цій статті я коротко розгляну можливості профілювання, вбудовані у Visual C++.

Насамперед необхідно встановити опції проекту для включення профілювання (тобто генерації профілювальної інформації). Це робиться через Project SettingsLinkEnable Profiling.

Далі виберіть BuildProfile, і з'явиться діалог "Profile", де можна вибрати будь-який тип профілювання, плюс ще є можливість як слід все це налаштувати за допомогою Custom Options (див. параметри команди PREP). Примітна також опція Merge - дозволяє поєднати поточні результати з попередніми для порівняння. Після натискання на "OK" запускається ваша програма - далі ви робите ті дії, які вам необхідноперевірити. Після завершення роботи вашої програми, профільна інформація виводиться у Profile Output Window, де ви її аналізуєте… і робите висновки.

І насамкінець, кілька порад.

1. Не варто профілювати весь додаток цілком, краще сконцентруватися на якихось окремих частинах, що становлять найбільший інтерес. (Див. параметри /EXC та /INC). Є багато частин, які просто немає сенсу профілювати - такі, як інтерфейс користувача, наприклад.

2. Виміри не завжди будуть точними, тому має сенс брати середнє від кількох проходів. Збирати статистику за кілька проходів можна за допомогою опції Merge.

3. Коли ви профільуєте, постарайтеся, щоб кількість запущених процесів у системі була мінімальною.

4. Краще від'єднатися від локальної мережі чи інтернету, щоб ОС не доводилося приймати вхідні пакети.

5. Слідкуйте за кількістю дзвінків. Наприклад, у вас в алгоритмі є ітерація на тисячу повторень – прослідкуйте, щоб функції, якими він користується, викликали відповідну кількість разів.

6. Час, протягом якого виконується функція, включає також час, протягом якого виконувались всі функції, викликані з даної.

Зацікавленим даною темою пропоную наступні статті у MSDN:

• Using Profile, PREP and PLIST

• Profiling from the Development Environment

Q.У мене питання для гуру. У конференціях кілька разів виникав, але якось так тихо і закінчувався. Чи то очевидна істина, чи ніхто не знає (чому я не вірю). Отже питання: як у програму на праву кнопку підчепити меню, таке саме як в Експлорері? Як туди напхати свої елементи? Друге друге питання відпадає, якщо можна вичепити саме меню, а не якусь системну функцію,яка виводить вікно меню, а тобі з цим доводиться упокорюватися, як із фактом буття цього екранного елемента…

A1 Контекстне (правокнопкове) меню створити досить просто:

2. Для крайнього зліва елемента верхнього рівня вводимо якесь ім'я і в отримане меню, що розкривається, додаємо команди.

3. Вставляємо обробник повідомлення WM_CONTEXTMENU в клас "вигляд" або в клас іншого вікна, що отримує повідомлення від кнопок миші, наприклад, в CMyDialog. Обробник цей програмуємо так:

CMyDialog::OnContextMenu(CWnd* pWnd, CPoint point)

menu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN TPM_RIGHTBUTTON, point.x, point.y, this);

От і все. TrackPopupMenu та займається виведенням контекстного меню на екран. Щоправда, об'єкт класу CMenu краще зробити мембером класу, тоді будь-якої функції можна буде видаляти, додавати, забороняти etc. пункти меню. Звісно, ​​у разі m_Menu.LoadMenu(IDR_MYMENU); треба написати в OnInitDialog. Зауважте, OnContextMenu отримує координати курсору, тобто. можемо для різних областей вікна просто виводити різні меню, просто перевіряючи координати.

Кожен об'єкт оболонки повинен реалізовувати COM-інтерфейс IShellFolder. Багато об'єктів реалізують і низку інших інтерфейсів. Так, IExtractIcon відповідає за іконку об'єкта, а IContextMenu – за його контекстне меню. Експлорер використовує ці (та інші) інтерфейси, щоб коректно відображати елементи ієрархії об'єктів та дозволяти користувачеві маніпулювати ними. Також ми можемо скористатися цими інтерфейсами.

Отже, нам потрібна функція, яка б виводила контекстне меню для заданого файлу (каталогу), який передавався б їй як параметр. Ця функція має виконати такі дії:

• Отримати інтерфейс 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);

else 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", point);

- Periodicals 1997, Microsoft Systems Journal, April, Wicked Code

– Опис IShellFolder та IContextMenu

Що стосується другого питання (про створення власних пунктів меню), ми маємо повний контроль над процесом створення меню, а отже, можемо робити з ним все, що завгодно. Потрібно лише мати на увазі 2 моменти.

По-перше, оскільки функція TrackPopupMenu викликається з прапором TPM_RETURNCMD, вона буде відправляти вікну повідомлення WM_COMMAND. Тому потрібно аналізувати значення nCmd, повернене функцією TrackPopupMenu та викликати потрібний обробник вручну. Наприклад:

UINT nCmd = PopupMenu.TrackPopupMenu(…);

if (nCmd == 0x8000)

По-друге, функція IContextMenu::QueryContextMenu отримує параметри idCmdFirst, idCmdLast (у прикладі вище вони дорівнюють 1 і 0x7FFF відповідно). Ідентифікатори для стандартних пунктів меню вибираються саме в діапазоні від idCmdFirst до idCmdLast. Тому потрібно простежити, щоб ідентифікатори пунктів меню користувача в цей діапазон не потрапили.

(Alexander Shargin)ЗВОРОТНИЙ ЗВ'ЯЗОК

Потрібно просто створити обробник події WM_ERASEBKGND з одного-єдиним рядком:

BOOL CSomeClass::OnEraseBkgnd(CDC* pDC)

Тобто, українською мовою, програма фон не очистила, малюйтеся повністю.

На відповідь A1 з минулого випуску:

Наразі практика. Нехай є готовий SDI додаток (з технологією Документ/Подання). Створюємо додаткове уявлення. Це робиться у функції CFrameWnd::OnCreateClient приблизно так:

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)

// class CNewView – це наше нове уявлення

// зверніть увагу на ідентифікатор нового подання

// змінна m_pNewView описана в CMainFrame як

m_pNewView = STATIC_DOWNCAST(CNewView, CreateView(pContext, AFX_IDW_PANE_FIRST+1));

m_pNewView->ShowWindow(SW_HIDE); // для скидання прапора WS_VISIBLE

return 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.

(Alexander Shargin)У ПОШУКАХ ІСТИНИ

Q.У мене dialog-base додаток, живе у systray. Необхідно, щоб програма при повторному запуску знаходила вже запущений екземпляр програми та активізувала його. Я намагався зробити це через FindWindow(), в яку передається ім'я зареєстрованого класу вікна, та заголовок вікна, яке розшукується. За заголовком я шукати не можу, тому що він весь час у мене змінюється. Отже, потрібно шукати за зареєстрованим ім'ям класу вікна. Ось тут і починається проблема. Я його не знаю. MFC сама їх роздає dialog-based додатків. А перевизначити це ім'я можна було б у PreCreateWindow(), але цей метод CDialog не успадковує із CWnd. В інших методах, ім'я класу вже зареєстровано, тобто. міняти його пізно. Як бути?