ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 25 от 26 ноября 2000 г.

Приветствую вас, уважаемые подписчики!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / /

Профилирование : анализ и оптимизация

Профилирование - это очень мощный инструмент анализа поведения вашей программы, позволяющий во время ее выполнения выявить "узкие места", где происходит падение производительности. С помощью профилирования можно получить статистику вызова функций, чтобы в дальнейшем использовать эту информацию при оптимизации кода. Ведь каждому понятно, что нет смысла оптимизировать функцию, вызывающуюся всего пару раз. При использовании MFC это тем более актуально, т.к. многие вызовы не всегда очевидны для программиста.

Еще одна причина использования профилирования - это контроль за качеством тестов. Такой контроль предоставляет информацию о том, осуществляет ли данный тест вызов и обработку результатов выполнения всех без исключения подпрограмм, и какие строки кода при этом выполняются.

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

Итак, профилирование используется для того, чтобы определить:

Приведу пример из жизни: в программе, осуществляющей доступ к данным из файла MS Access, наблюдалось катастрофичное падение производительности при превышении объема данных определенной величины, причем относительно небольшой (порядка 500 записей). Применение профилирования позволило мгновенно выяснить, что подпрограмма, извлекающая данные из базы, совершенно здесь не при чем, а всему виной подпрограмма, заносящая данные в таблицу на экране. После внесения соответствующих изменений, все отлично (т.е. БЫСТРО) заработало, причем скорость программы при просмотре записей практически перестала зависеть от объема данных.

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

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

  • суммарный объем времени, в течение которого выполнялась функция + количество вызовов этой функции (function timing),
  • только количество вызовов функции (function counting),
  • список ни разу не вызывавшихся функций (function coverage),
  • запись содержимого стека при каждом вызове функции (function attribution).

    Профилирование по строкам используется для проверки алгоритмов, т.к. позволяет посмотреть, сколько раз была выполнена каждая строчка, а также выявить строки, не выполнившиеся вообще ни разу. Здесь есть только два варианта: подсчет строк (line counting) - т.е. сколько раз данная строка была выполнена; и покрытие строк (line coverage) - показывает те строки, которые выполнялись хотя бы раз.

    Перейдем к практике. Если у вас установлен Visual C++ Professional или Enterprise Edition, то профилировщик у вас есть, он встроен в IDE. Осталось только научиться им пользоваться. Предугадывая поток писем, замечу, что существует довольно большой выбор всяческих профилировщиков от сторонних фирм, возможности которых иногда действительно впечатляют. Но в данной статье я кратко рассмотрю возможности профилирования, встроенные в Visual C++.

    Прежде всего необходимо установить опции проекта для включения профилирования (т.е. генерации профилировочной информации). Это делается через Project Settings|Link|Enable Profiling.

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

    И напоследок, несколько советов.

    Заинтересовавшимся данной темой предлагаю следующие статьи в MSDN:

  • Performance Tuning
  • Using Profile, PREP and PLIST
  • Profiling from the Development Environment

    / / / / ВОПРОС-ОТВЕТ / / / / / / / / /

    Q| У меня вопрос для гуру. В конференциях от пару раз возникал, но как-то так тихо и кончался. То ли это очевидная истина, то ли никто не знает (чему я не верю). Итак вопрос: как в программу на правую кнопку подцепить меню, такое же как в Експлорере? Как туда напихать свои элементы? Второй второй вопрос отпадает, если можно выцепить именно меню, а не какую-то системную функцию, которая выводит окно меню, а тебе с этим сприходится смиряться, как с фактом бытия этого экранного элемента.... - Serg Loginov

    |A1 Контекстное (правокнопочное) меню создать довольно просто:
    1. Добавляем в графическом редакторе новое пустое меню.
    2. Для крайнего слева элемента верхнего уровня вводим какое-нить имя и в полученное раскрывающееся меню добавляем команды.
    3. Вставляем обработчик сообщения WM_CONTEXTMENU в класс "вид" или в класс другого окна, получающего сообщения от кнопок мыши, ну, например, в CMyDialog. Обработчик этот программируем так:

    CMyDialog :: OnContextMenu ( CWnd* pWnd, CPoint point ) { CMenu menu; menu.LoadMenu ( IDR_MYMENU ); menu.GetSubMenu ( 0 ) -> TrackPopupMenu ( TPM_LEFTALIGN | TPM_RIGHTBUTTON , point.x , point.y , this ); }

    Вот и все. TrackPopupMenu и занимается выводом контекстного меню на экран. Правда, объект класса CMenu лучше сделать мембером класса, тогда в любой функции можно будет удалять, добавлять, запрещать etc. элементы меню. Конечно, в этом случае m_Menu.LoadMenu ( IDR_MYMENU ); надо написать в OnInitDialog. Заметте, OnContextMenu получает координаты курсора, т.е. можем для разных областей окна элементарно выводить разные меню, просто проверяя координаты.

    - Sergey Pochechuev

    |A2 В Windows все файлы и папки входят в иерархию объектов оболочки. В неё также входят и объекты, не имеющие отношение к файловой системе: корзина, рабочий стол и т. п. Windows Explorer является по сути программой для просмотра этой иерархии объектов.

    Каждый объект оболочки обязан реализовывать 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)
    {
       // Строим полное имя.
       TCHAR tchPath[MAX_PATH];
       GetFullPathName(pszPath, sizeof(tchPath)/sizeof(TCHAR), tchPath, NULL);
    
       // Если нужно, перекодируем ANSI в UNICODE.
       WCHAR wchPath[MAX_PATH];
       if(IsTextUnicode (tchPath, lstrlen (tchPath), NULL))
          lstrcpy ((char *) wchPath, tchPath);
       else
          MultiByteToWideChar(CP_ACP, 0, pszPath, -1, wchPath,
                              sizeof(wchPath)/sizeof(WCHAR));
    
       // Получаем интерфейс IShellFolder рабочего стола
       IShellFolder *pDesktopFolder;
       SHGetDesktopFolder(&pDesktopFolder);
    
       // Преобразуем путь в LPITEMIDLIST
       LPITEMIDLIST pidl;
       pDesktopFolder->ParseDisplayName(pWnd->m_hWnd, NULL, wchPath, NULL, &pidl, NULL);
    
       // Получаем интерфейс IShellFolder для заданного файла (папки)
       IShellFolder *pFolder;
       pDesktopFolder->BindToObject(pidl, NULL, IID_IShellFolder, (void**)&pFolder);
    
       // Получаем интерфейс IContextMenu для заданного файла (папки)
       IContextMenu *pContextMenu;
       pFolder->GetUIObjectOf(
          pWnd->m_hWnd, 1, (LPCITEMIDLIST *)&pidl, IID_IContextMenu, NULL, (void**)&pContextMenu);
    
       // Создаём меню
       CMenu PopupMenu;
       PopupMenu.CreatePopupMenu();
    
       // Заполняем меню
       pContextMenu->QueryContextMenu(PopupMenu.m_hMenu, 0, 1, 0x7FFF,CMF_EXPLORE);
    
       // Отображаем меню
       UINT nCmd = PopupMenu.TrackPopupMenu(
          TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_RIGHTBUTTON|TPM_RETURNCMD,
          point.x, point.y, pWnd);
    
       // Выполняем команду (если она была выбрана)
       if(nCmd)
       {
          CMINVOKECOMMANDINFO ici;
          ZeroMemory(&ici, sizeof(CMINVOKECOMMANDINFO));
          ici.cbSize = sizeof(CMINVOKECOMMANDINFO);
    
          ici.hwnd = pWnd->m_hWnd;
          ici.lpVerb = MAKEINTRESOURCE(nCmd-1);
          ici.nShow = SW_SHOWNORMAL;
    
          pContextMenu->InvokeCommand(&ici);
       }
    
       // Получаем интерфейс IMalloc.
       IMalloc *pMalloc;
       SHGetMalloc(&pMalloc);
    
       // Используем его для освобождения памяти, выделенной на ITEMIDLIST
       pMalloc->Free(pidl);
    
       // Освобождаем все полученные интерфейсы
       pDesktopFolder->Release();
       pFolder->Release();
       pContextMenu->Release();
       pMalloc->Release();
    
       return;
    }
    

    Эту функцию можно вызывать, например, из обработчика OnContextMenu. Делается это так:

    void CMyView::OnContextMenu(CWnd* pWnd, CPoint point) { ShowContextMenu (pWnd, "C:\\command.com", point); }

    За дополнительной информацией следует обратиться к следующим статьям в MSDN:
    - Periodicals 1997, Microsoft Systems Journal, April, Wicked Code
    - Knowledge Base, статья ID: Q198288
    - Описание IShellFolder и IContextMenu

    Что касается второго вопроса (о создании собственных пунктов меню), мы имеем полный контроль над процессом создания меню, а значит можем делать с ним всё, что угодно. Нужно только иметь в виду 2 момента.
    Во-первых, поскольку функция TrackPopupMenu вызывается с флагом TPM_RETURNCMD, она на будет отправлять окну сообщение WM_COMMAND. Поэтому нужно анализировать значение nCmd, возвращённое функцией TrackPopupMenu и вызывать нужный обработчик вручную. Например:
    
    UINT nCmd = PopupMenu.TrackPopupMenu(...);
    
    if(nCmd)
    {
       if(nCmd == 0x8000)
       {
          AfxMessageBox("It works!!!");
       }
       else
       {
          // Используем IContextMenu::InvokeCommand
       }
    }
    
    Во-вторых, функция IContextMenu::QueryContextMenu получает параметры idCmdFirst, idCmdLast (в примере выше они равны 1 и 0x7FFF соответственно). Идентификаторы для стандартных пунктов меню выбираются именно в диапазоне от idCmdFirst до idCmdLast. Поэтому нужно проследить, чтобы идентификаторы пользовательских пунктов меню в этот диапазон не попали.

    - Alexander Shargin

    / / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / /

    Когда начал читать вашу статью на тему мерцания, подумал было, что вы обязательно упомянете тот метод, который использовал я в своей программе. На мой взгляд, он достаточно известен, и, кажется, является самым самым лучшим.
    Нужно просто создать обработчик события WM_ERASEBKGND с одной-единственной строчкой:

    BOOL CSomeClass::OnEraseBkgnd(CDC* pDC)
    {
    return FALSE;
    }
    Т. е., по-русски говоря, программа фон не очистила, рисуйтесь полностью.

    - Vlad



    На ответ A1 из прошлого выпуска:
    Теперь практика. Пусть имеется готовое SDI приложение (с технологией Документ/Представление). Создаем дополнительное Представление. Это делается в функции CFrameWnd::OnCreateClient примерно так:
    
    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
    pContext)
    {
    // class CNewView - это наше новое представление
      pContext->m_pNewViewClass = RUNTIME_CLASS(CNewView);
    // обратите внимание на идентификатор нового Представления
    // переменная m_pNewView описанна в CMainFrame как CNewView* m_pNewView;
      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->m_pNewViewClass уже изменилось! В результате вместо двух разных видов будет создано два одинаковых. Ошибка лечится переносом вызова функции из базового класса в начало переопределённой функции:
    
    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
    pContext)
    {
      int nResult = CFrameWnd::OnCreateClient(lpcs, pContext);
      ...
      return nResult;
    }
    
    Кроме того, неясно, как использовать функцию SwitchView. Указатель на созданный нами вид хранится в m_pNewView, но для получения указателя на вид, созданный самой MFC, не видно удобного способа. Вероятно, лучший вариант - также сохранить его в члене класса CMainFrame.

    - Alexander Shargin

    / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / /

    Q| У меня dialog-base приложение, живет в systray. Необходимо, чтобы приложение при повторном запуске находило уже запущеный экземпляр программы и активизировало его. Я пытался сделать это через FindWindow(), в которую передается имя зарегистрированного класса окна, и заголовок окна, которое разыскивается. По заголовку я искать не могу, так как он все время у меня меняется. Следовательно, нужно искать по зарегистрированному имени класса окна. Вот тут то и начинается проблема. Я его не знаю. MFC сама их раздает dialog-based приложениям. А переопределить это имя можно было бы в PreCreateWindow(), но этот метод CDialog не наследует из CWnd. Во всех остальных методах, имя класса уже зарегистрированно, т.е. менять его поздно. Как быть? - el-f

       Ответить на вопрос

    / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

    Это все на сегодня. До новых встреч!

    Алекс Jenter   jenter@mail.ru
    Красноярск, 2000.

    Предыдущие выпуски     Статистика рассылки