Статистика использования памяти. Реализация в ascLib.

Автор: Станислав Михайлов
Оптим.ру

Источник: RSDN Magazine #1
Опубликовано: 10.12.2002
Версия текста: 1.0

ascLib

Иногда на этапе окончательной отладки кажется, что программа работает безошибочно… но как-то слишком медленно. Конечно, можно посоветовать заказчику сделать очередной upgrage, но вряд ли подобное предложение его обрадует. Поэтому, лучше для начала попытаться понять, что же приводит к замедлению работы программы, и устранить проблему.

Общим решением определения общих мест является профайлинг. Средства профайлинга встроены во многие средства разработки, а также доступны в виде отдельных продуктов типа VTune Performance Analyzer от Intel. Но эта статья о другом. Тот же VTune может найти узкие места, но показать общей картины производительности приложения он не в силах. Если же речь заходит о глубоком исследовании одной из составляющих производительности, такие средства и вовсе бессильны.

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

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

Очевидно, что перед тем, как начинать бороться «за ускорение», желательно убедиться, что память не «протекает» (см. статью «Как отслеживать потери памяти с помощью библиотеки ascLib» в предыдущем номере RSDN). После этого нужно каким-то образом собрать статистическую информацию об использования памяти в программе: когда и сколько блоков какого размера было занято и освобождено, сколько их было занято всего и, сколько – одновременно («пиковая» нагрузка).

Оказывается, это не очень просто. Даже в небольших приложениях бывает трудно проследить за всеми заемами и освобождениями памяти простым проходом под отладчиком или анализом текста программы. Большая же программа может выполнять миллионы операций с памятью за несколько минут – совершенно невозможно контролировать их «вручную». Значит, разумно накапливать информацию о заемах и освобождениях памяти в специальном буфере. После завершения работы программы эту информацию можно будет вывести в файл и просмотреть в графическом виде (например, с помощью Microsoft Excel).

Именно такой принцип положен в основу статистики, реализованной в ascLib. Все необходимое для работы статистики (кроме переопределений функций работы с памятью и самих вызовов функций статистики) находится в файлах: “ascMemStatistic.h”, "ascMemStatistic.cpp", "ascInterval.h", "ascMacro.h", "ascHeap.h" и "ascTrace.h".

Для хранения информации о работе памяти создается специальный динамический массив g_arrMemStatBlockInfo (класс CascMemStatBlockInfoArray). Элементами массива являются структуры CascMemStatBlockInfo, состоящие из четырех полей: адрес блока памяти m_pAddress, размер этого блока m_dwSize, время вызова операции m_dwTickCount, тип операции m_amsaAction (занятие или освобождение памяти). Описание этой структуры и используемого в ней перечисления ASC_ MEM_STAT_ACTION приведено ниже:

// Контролируемые виды операций с памятью
typedef enum ASC_MEM_STAT_ACTION
{
  amsaUnknown = 0, // значение для инициализации (сигнализирует об ошибке)
  amsaAlloc,
  amsaFree
} ASC_MEM_STAT_ACTION;

// Структура для хранения статистической информации
typedef struct CascMemStatBlockInfo
{
  void * m_pAddress;
  ASC_MEM_STAT_ACTION m_amsaAction;
  DWORD m_dwSize;
  DWORD m_dwTickCount;
} CascMemStatBlockInfo;

В массиве реализован свой собственный heap – все функции статистики для своих внутренних нужд занимают память только в нем (ну, и в стеке, конечно же). Это сделано для максимального отчуждения работы статистики от хода выполнения основной программы.

Чтобы получить контроль над выделением и освобождением памяти, все соответствующие операторы и функции языка C++ переопределены (new, delete, mallioc, free, realloc, _expand, calloc). При вызове любого из них происходит добавление элемента в массив g_arrMemStatBlockInfo (для этого используется функция ascMemStatisticAppend). Пример переопределения функций работы с памятью для этих целей приведен ниже:

// Предопределение функции ascMemStatisticAppend
// Тело этой функции будет приведено ниже
inline HRESULT ascMemStatisticAppend(
  void * pAddress, ASC_MEM_STAT_ACTION amsaAction, DWORD dwSize);

// Переопределение malloc
inline LPVOID MyMalloc(DWORD dwBytes)
{
  LPVOID pMem = malloc(dwBytes);
  ATLASSERT(lpMem);
#ifdef ASC_MEM_STATISTIC
  if(pMem)
    ascMemStatisticAppend(pMem, amsaAlloc, dwSize);
#endif //ASC_MEM_STATISTIC
  return pMem;
}
#define malloc(n) MyMalloc(n)

// Переопределение free
inline void MyFree(LPVOID pMem)
{
  if(pMem)
  {
#ifdef ASC_MEM_STATISTIC
    ascMemStatisticAppend(pMem, amsaFree, 0);
#endif //ASC_MEM_STATISTIC
    free(pMem);
  }
}
#define free(p) MyFree(p)

// Переопределение realloc
inline LPVOID MyReAlloc(LPVOID lpMem, DWORD dwBytes)
{
#ifdef ASC_MEM_STATISTIC
  if(pMem)
    ascMemStatisticAppend(pMem, amsaFree, 0);
#endif //ASC_MEM_STATISTIC
  lpMem = realloc(lpMem, dwBytes);
  ATLASSERT(lpMem);
#ifdef ASC_MEM_STATISTIC
  if(pMem)
    ascMemStatisticAppend(pMem, amsaAlloc, dwSize);
#endif //ASC_MEM_STATISTIC
}
#define realloc(p, n) MyReAlloc (p, n)

Накопление статистической информации начинается вызовом функции ascMemStatisticStart (описание см. ниже), которая инициализирует массив g_arrMemStatBlockInfo. В ascLib эта функция автоматически вызывается при инициализации модуля, причем имя файла модуля передается в параметрах szaCaption и szaLogFileName.

Завершение сбора статистической информации происходит при вызове функции ascMemStatisticFinish (описание см. ниже). При ее вызове автоматически формируется отчет и освобождаются все ресурсы, использовавшиеся для сбора статистики. В ascLib эта функция вызывается при завершении работы модуля. Отчет по текущему состоянию памяти можно получить и в любой другой момент выполнения программы, вызвав функцию ascMemStatisticReport (описание см. ниже). Эта функция не прекращает накопления статистической информации.

Вот основной код этих функций и используемые в них определения:

// *** Пояснение к тексту программы:
// класс CascTraceData и объявление ASC_TRACE_TARGET_DEFAULT описаны
// в файле “ascTrace.h”. Более подробную информацию об этом классе читайте
// в вышеупомянутой статье («Как отслеживать потери памяти…»).

#include "ascMacro.h"  // Основные макросы ascLib
#include "ascInterval.h" // Функции работы с интервалами значений
#include "ascHeap.h" // heap для независимой от ascLib работы с памятью
#include "ascTrace.h" // Только для использования класса CascTraceData

// Определяет шаблон названия (основу имени) log-файла.
#define g_cszaMemStatisticReportFileDefaultName "ascMemStatReport"
// Определяет полный путь до загружаемого (входного) файла с настройками
// работы статистики. Если NULL, то берутся настройки по умолчанию.
// Если файл указан, но не найден - то берутся настройки по умолчанию
// и выдается ASSERT.
#define g_cszaMemStatisticReportParamFileInDefaultFullPath \
  "C:\\Temp\\ascMemStatisticReport\\ascMemStatParamIn.dat"
// Определяет полный путь до выводимого (выходного) файла
// с настройками работы статистики.
#define g_cszaMemStatisticReportParamFileOutDefaultFullPath \
  "C:\\Temp\\ascMemStatisticReport\\ascMemStatParamOut.dat"

// Список флагов, определяющих возможные типы отчетов
// Описание этих типов приведено далее в статье
typedef enum ASC_MEM_STAT_REPORT_TYPE
{
  amsrtOverview = 1,
  amsrtBlockSize = 2,
  amsrtLoadPeak = 4,
  amsrtTimeInterval = 8,
  amsrtLogFile = 16,
  amsrtFull = amsrtOverview | amsrtBlockSize | amsrtLoadPeak
   | amsrtTimeInterval | amsrtLogFile
} ASC_MEM_STAT_REPORT_TYPE;

// Возможные типы сортировки данных в отчете
// Описание этих типов приведено далее в статье
typedef enum ASC_MEM_STAT_REPORT_SORT_TYPE
{
  amsrstUnsorted = 0,
  amsrstSortByCount,
  amsrstSortBySize
} ASC_MEM_STAT_REPORT_SORT_TYPE;

// Вызывается перед началом накопления стат. информации 
inline HRESULT ascMemStatisticStart()
{
  // Проинициализировать массив для начала накопления информации
  return g_arrMemStatBlockInfo.Init();
}

// Позволяет добавить учетную запись о занятии или освобождении памяти
inline HRESULT ascMemStatisticAppend(
  void * pAddress, ASC_MEM_STAT_ACTION amsaAction, DWORD dwSize)
{
  // Если массив еще не проинициализирован, то не добавлять данные
  if(!g_arrMemStatBlockInfo.Inited())
    return S_FALSE;

  // Добавить запись в массив
  // Для типа amsaFree значение dwSize будет определено автоматически
  return g_arrMemStatBlockInfo.Append(
    pAddress, amsaAction, dwSize, GetTickCount());
}

// Позволяет выдать отчет(ы) о работе с памятью
// amsrt определяет тип отчета (см. пояснения к ASC_MEM_STAT_REPORT_TYPE)
// szaCaption определяет заголовок отчета при его выдаче через CascTraceData
// patd определяет CascTraceData, через который выдается отчет
// (в debug-window, clip-board или строку)
// szaLogFileName нужен только для случая (amsrt & amsrtLogFile)
inline HRESULT ascMemStatisticReport(
  LPSTR szaCaption = NULL,
  DWORD dwTarget = ASC_TRACE_TARGET_DEFAULT,
  CascTraceData *patd = NULL,
  LPSTR szaLogFileName = g_cszaMemStatisticReportFileDefaultName)
{ 
  if(!g_arrMemStatBlockInfo.Inited())
    return E_FAIL;
  // Получить настройки отчетов из файла или по умолчанию
  CascMemStatParam params;
  ascMemStatisticParamLoad(
    g_cszaMemStatisticReportParamFileInDefaultFullPath, params); 
  // Выделить только имя файла из, возможно, полного пути до него
  szaLogFileName = ascFileNameFromPathA(szaLogFileName, FALSE);
  ATLASSERT(szaLogFileName && ascStrLen(szaLogFileName));
  // Определяем, используется внешний объект CascTraceData, или внутренний
  CascTraceData ascTraceData(dwTarget);
  CascTraceData *patdLoc = patd ? patd : &ascTraceData;
  // Вывести общий заголовок для всех отчетов
  if(szaCaption)
  {
    patdLoc->SendOut("\r\n");
    patdLoc->SendOut(szaCaption);
  }
  // Определить, есть ли данные в массиве
  int iBlockCount = g_arrMemStatBlockInfo.Count();
  if(!iBlockCount)
  { // Сообщить, что данных нет, сохранить настройки и выйти
    patdLoc->SendOut("\r\nINFO: memory usage statictic array is empty\r\n");
    ascMemStatisticParamSave(
      g_cszaMemStatisticReportParamFileOutDefaultFullPath, params);
    return S_FALSE;
  }
  // Сообщить о начале выдачи отчетов и вывести их
  // (типы отчетов описаны ниже в тексте статьи)
  patdLoc->SendOut(
    "\r\nINFO: +++++++++++++”
    “begin of memory usage statictic array  +++++++++++++\r\n");
  if(amsrtOverview & params.m_dwReportTypeFlags)
    ascMemStatisticReportOverview(patdLoc);
  if(amsrtBlockSize & params.m_dwReportTypeFlags)
    ascMemStatisticReportBlockSize(params.m_dwReportSortFlags, patdLoc);
  if(amsrtLoadPeak & params.m_dwReportTypeFlags)
    ascMemStatisticReportLoadPeak(params.m_dwReportSortFlags, patdLoc);
  if(amsrtTimeInterval & params.m_dwReportTypeFlags)
    ascMemStatisticReportTimeInterval(
      params.m_dwReportSortFlags, patdLoc,
      params.m_dwIntervalFactor, params.m_dwIntervalBase,
      params.m_IntervalType
    );
   if(amsrtLogFile & params.m_dwReportTypeFlags)
      ascMemStatisticReportLogFile(
        patdLoc, szaLogFileName,
        params.m_szaReportFileExtension,
        params.m_szaReportFileDir,
        params.m_szaReportFileDelimiter,
        params.m_bMemReportFileOnceFile,
        params.m_bMemReportFileGroupByInterval,
        params.m_dwIntervalFactor,
        params.m_dwIntervalBase,
        params.m_IntervalType,
        params.m_IntervalPredefinedArray,
        params.m_dwIntervalPredefinedArrayCount,
        params.m_dwIntervalPredefinedArrayLowerBound
      );
  // Сообщить о завершении выдачи отчетов
  patdLoc->SendOut(
    "INFO: +++++++++++++”
    “end of memory usage statictic array +++++++++++++\r\n\r\n");
  // Cохранить настройки
  ascMemStatisticParamSave(
    g_cszaMemStatisticReportParamFileOutDefaultFullPath, params);
  return S_OK;
}

// Про параметры см. комментарии к ascMemStatisticReport
inline HRESULT ascMemStatisticFinish(
  LPSTR szaCaption = NULL,
  DWORD dwTarget = ASC_TRACE_TARGET_DEFAULT,
  CascTraceData *patd = NULL,
  LPSTR szaLogFileName = g_cszaMemStatisticReportFileDefaultName)
{
  // Выдать статистику
  ascMemStatisticReport(szaCaption, dwTarget, patd, szaLogFileName);
  // Очистить массив со статистической информацией
  return g_arrMemStatBlockInfo.Clear();
}

Добавление информации производится по мере выделения/освобождения памяти, при этом запоминается время каждой операции. Время определяется относительно начала накопления данных и измеряется в миллисекундах с помощью системной функции GetTickCount.

Мало реализовать сам сбор данных. Нужно ещё и показать их в виде, доступном для понимания.

В ascLib преобразование статистической информации из массива g_arrMemStatBlockInfo в читаемый вид осуществляется во время создания отчетов. Все отчеты в ascLib построены по принципу раздельного показа статистики для блоков памяти разного размера. Предусмотрено несколько вариантов отчетов, которые могут комбинироваться. Они описаны в перечислении ASC_MEM_STAT_REPORT_TYPE:

amsrtOverview - обобщенная статистика.

Показывается общее количество выделений и освобождений памяти за все время работы программы в нескольких диапазонах размеров блоков.

amsrtBlockSize – список количества занятий памяти для разных размеров блоков.

amsrtLoadPeak - список пиковых загрузок.

Показывается максимальное количество (пики) занятий блоков памяти, существующих (занятых и не отпущенных) одновременно. Выводится одно значение (максимальный пик) для каждого из встречающихся размеров блоков.

amsrtTimeInterval – список пиковых загрузок в различных количественных интервалах.

Выводится список встречающихся размеров блоков. Для каждого из них считается количество одновременно занятых блоков. Это количество при подсчете группируется поинтервально (например, от 0 до 1, от 1 до 2-х, от 2-х до 5-ти, от 5-ти до 10-ти и т.д.). Для каждого интервала определяется суммарное время, в течении которого этот интервал существовал (то есть сколько времени были заняты, скажем, от 2-х до 5-ти блоков каждого размера).

Список интервалов и времен их существования выводится для каждого из размеров блоков.

amsrtLogFile – список всех занятий и освобождений памяти, выводимый в хронологической последовательности.

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

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

amsrtFull – выводить все отчеты.

Отчеты типов amsrtOverview, amsrtLoadPeak и amsrtTimeInterval по умолчанию выводятся в debug-window, но можно перенаправить их в буфер обмена или получить в строку (последнее, разумеется, доступно только из кода программы). Направление вывода данных определяется флагами, передаваемыми в экземпляр класса CascTraceData при его создании.

Эти отчеты имеют небольшой размер, поэтому вывод их в файл не реализован. Результат выполнения всех отчетов выводится в debug-window (при Debug-компиляции) или в буфер обмена (при Release-компиляции). В случае типа amsrtFull эта информация выглядит так (на примере библиотеки TestDll001.dll):

E:\MyProjects\Tests\TestDll001\Debug\TestDll001
INFO: +++++++++++++ begin of memory usage statictic array  +++++++++++++
-------- Report type: 'Overview' (full count, full size and max. peak size for loading memory blocks)
   Full count: 28;   Full size: 13324;   Average size: 475
   Max. peak size: 10510
-------- Report type: 'BlockSize' (full count for loading memory blocks of the same size)
   count: 11;   size: 10;
   count: 2;    size: 100;
   count: 2;    size: 257;
   count: 1;    size: 500;
   count: 12;   size: 1000;
-------- Report type: 'LoadPeak' (peak count for memory blocks loading by different size: max. quantity of simultaneously loaded bloks of the same size)
   count: 5;    size: 10;
   count: 1;    size: 100;
   count: 1;    size: 257;
   count: 1;    size: 500;
   count: 10;   size: 1000;
-------- Report type: 'TimeInterval' (loading time intervals for same size of memory blocks)
   10 (block size)
      by count 0 - 1:   130 ms (33 percent of full time);
      by count 1 - 2:   150 ms (38 percent of full time);
      by count 2 - 3:   60 ms (15 percent of full time);
      by count 3 - 5:   20 ms (5 percent of full time);
   100 (block size)
      by count 0 - 1:   20 ms (5 percent of full time);
   257 (block size)
      by count 0 - 1:   20 ms (5 percent of full time);
   500 (block size)
      by count 0 - 1:   110 ms (28 percent of full time);
   1000 (block size)
      by count 0 - 1:   150 ms (38 percent of full time);
      by count 1 - 2:   10 ms (2 percent of full time);
      by count 2 - 3:   10 ms (2 percent of full time);
      by count 3 - 5:   20 ms (5 percent of full time);
      by count 5 - 7:   30 ms (7 percent of full time);
      by count 7 - 10:  30 ms (7 percent of full time);
   *** Full time: 390 ms ***
-------- Report type: 'LogFile' (trace memory usage to log-files by block size)
   Log-file name: C:\Temp\ascMemStatisticReport\TestDll001.log  SUCCEEDED
INFO: +++++++++++++ end of memory usage statictic array +++++++++++++

Отчеты типа amsrtLogFile всегда выводятся в файл, поскольку могут иметь значительные объемы. В debug-window (или в буфер обмена) выводится только информация об успехе или неудаче создания отчетного файла, и указывается полный путь до него. Содержимое этого отчетного файла представлено ниже (на примере библиотеки TestDll001.dll):

ms  _10 ms  _100    ms  _257    ms  _500    ms  _1000
20  0   370 0   160 0   30  0   0   0
20  1   370 1   160 1   30  1   0   1
140 1   380 1   170 1   140 1   40  1
140 0   380 0   170 0   140 0   40  3
140 0   380 0   210 0           60  3
140 1   380 1   210 1           60  6
150 1   390 1   220 1           100 6
150 3   390 0   220 0           100 10
190 3                           140 10
190 6                           140 6
210 6                           140 6
210 3                           140 3
380 3                           140 3
380 1                           140 1
380 1                           140 1
380 0                           140 0
                                240 0
                                240 1
                                250 1
                                250 0
                                270 0
                                270 1
                                370 1
                                370 0

В файле отчета чередуются колонки с временем использования памяти и количеством блоков указанного размера. Чтобы уменьшить объем информации, количество блоков группируется по некоторому (задаваемому) принципу. Речь об этом пойдет ниже. В приведенном выше примере использовалась группировка по интервалам: от 0 до 1-го, от 1-го до 3-х, от 3-х до 6-ти и от 6-ти до 10-ти, причем занимались блоки размером 10, 100, 257, 500 и 1000 байт. Время представляет время нахождения в одном интервале

Отчетный файл с данными статистики может быть загружен в Excel, где по нему строятся графики работы с памятью. Чтобы не заниматься ручным трудом, можно строить графики с помощью макроса. Например, такого:

' Макрос для Excel
' Создает графики для набора парных колонок
' В паре: первая колонка - ось X, вторая - ось Y
' Используется для визуализации результатов ascMemStatistic
‘(статистика работы памяти, см. "ascLib\ascMemStatistic.h")
Sub CreateGraphic()
  Dim i As Long, iColCount As Long
  For i = 1 To Columns.Count
    If Columns(i).Cells(1).Text = "" Then
      iColCount = i - 1
      i = Columns.Count
    End If
  Next
  If (iColCount < 1) Then Exit Sub
  ' Количество колонок должно быть четным!
  If (0 <> iColCount Mod 2) Then Exit Sub
  ' Уменьшить кол-во вдвое
  iColCount = iColCount / 2
  ReDim rgsName(1 To iColCount) As String
  ReDim rgsTime(1 To iColCount) As Range
  ReDim rgsCnt(1 To iColCount) As Range
  Dim iA As Long, iCell As Long
  Dim iAPre As Long, iAPreOld As Long
  Dim iZ_A As Long, sPre As String
  Dim iAppend As Long
  iA = Asc("A")
  iZ_A = Asc("Z") - Asc("A") + 1
  iCell = 1: iAppend = 0: sPre = ""
  iAPre = 0: iAPreOld = 0
  For i = LBound(rgsTime) To UBound(rgsTime)
    iAPre = Int(iCell / iZ_A)
    If (iAPre <> iAPreOld) Then
      sPre = Chr(iAPre + iA - 1)
    End If
    iAppend = iCell - (iAPre * iZ_A) + iA
    rgsName(i) = Range(sPre & Chr(iAppend) & "1").Text
    Range(sPre & Chr(iAppend - 1) & "2").Select
    Set rgsTime(i) = Range(Selection, Selection.End(xlDown))
    Range(sPre & Chr(iAppend) & "2").Select
    Set rgsCnt(i) = Range(Selection, Selection.End(xlDown))
    iAPreOld = iAPre
    iCell = iCell + 2
  Next
  Dim SheetCur As Object
  Set SheetCur = ActiveSheet
  Charts.Add
  ActiveChart.ChartType = xlXYScatterLinesNoMarkers
  ActiveChart.Location Where:=xlLocationAsObject, Name:=SheetCur.Name
  If ActiveChart.SeriesCollection.Count > 0 Then
    For i = ActiveChart.SeriesCollection.Count To 1
      ActiveChart.SeriesCollection(i).Delete
    Next
  End If
  Dim ser As Object
  For i = LBound(rgsTime) To UBound(rgsTime)
    ActiveChart.SeriesCollection.Add rgsTime(i)
    Set ser = ActiveChart.SeriesCollection(i)
    ser.Name = rgsName(i)
    ser.XValues = rgsTime(i)
    ser.Values = rgsCnt(i)
  Next
  With ActiveChart
    .HasTitle = True
    .ChartTitle.Characters.Text = "Statistics of memory usage"
    .Axes(xlCategory, xlPrimary).HasTitle = True
    .Axes(xlCategory, xlPrimary).AxisTitle.Characters.Text = "Count"
    .Axes(xlValue, xlPrimary).HasTitle = True
    .Axes(xlValue, xlPrimary).AxisTitle.Characters.Text = "Time, ms"
  End With
End Sub

В результате выполнения этого макроса Excel строит график, который выглядит следующим образом:


Рисунок 1. Статистика использования памяти в библиотеке Test001.dll.

На графике видно, что, например, программой на короткое время занимается от 6 до 10 блоков размером 1000 байт, и половину времени работы программа не отпускает 3 блока размером 10 байт. Разумеется, это всего лишь пример. Библиотека Test001.dll между инициализацией и завершением модуля не выполняет никаких других действий, кроме тестовых занятий и освобождений памяти.

График реально работающей программы выглядит несколько иначе (библиотека ascDbShared.dll из поставки ascDb в реальном приложении):


Рисунок 2. Статистика использования памяти в библиотеке ascDbShared.dll.

Для включения самого режима статистики в ascLib нужно в настройках проекта или перед включением файла “ascLibInit.h” объявить ASC_MEM_STATISTIC. Статистика не будет работать, если выключено переопределение операторов выделения и освобождения памяти (не должно быть объявлено _NO_ASC_MEM_REDIRECT).

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

Для настройки статистики служат следующие параметры:

Тип отчета – определяется набором флагов из перечисления ASC_MEM_STAT_REPORT_TYPE.

Значение этого параметра по умолчанию определяется объявлением:

#define ASC_MEM_STATISTIC_REPORT_TYPE_DEFAULT amsrtFull

В файле с настройками ему соответствует набор параметров Report_Overview_Enabled, Report_BlockSize_Enabled, Report_LoadPeak_Enabled, Report_TimeInterval_Enabled и Report_LogFile_Enabled (каждый типа Boolean).

Тип сортировки – способ сортировки значений в отчетах типа amsrtBlockSize, amsrtLoadPeak и amsrtTimeInterval. Определяется значением из перечисления ASC_MEM_STAT_REPORT_SORT_TYPE.

Если указано amsrstUnsorted, то сортировки не будет. Если amsrstSortByCount, то сортировка будет производиться по количеству заемов памяти. Если указано amsrstSortBySize – то по размеру блоков. Значение этого параметра по умолчанию определяется объявлением:

#define ASC_MEM_STATISTIC_SORT_TYPE_DEFAULT amsrstSortBySize

В файле с настройками ему соответствует параметр Report_Sort (можно указывать значение или имя элемента из перечисления).

Группирование по интервалам – группировать или нет значения по интервалам в отчетах типа amsrtTimeInterval и amsrtLogFile (если True, то группировать). Значение этого параметра по умолчанию определяется объявлением:

#define g_bMemStatisticReportFileDefaultGroupByInterval TRUE

В файле с настройками ему соответствует параметр Report_GroupByInterval (тип Boolean).

Тип интервала – тип интервала, если включена группировка по интервалам (иначе игнорируется). Это значение из перечисления ASC_INTERVAL_TYPE:

typedef enum ASC_INTERVAL_TYPE
{
  aitLinear = 1,
  aitPower,
  aitArray
} ASC_INTERVAL_TYPE; 

Если указано aitLinear, то при рассчете интервалов будет использоваться линейная функция вида x = k*n. Если указано aitPower – то степенная функция вида x = k * a^n. В обоих случаях “x” - искомое значение верхней границы интервала, “k” – множитель интервала (см. ниже),“n” – целочисленный индекс интервала, “а” - основание степени в степенной функции. Если указано aitArray, то значения границ интервала берутся из предопределенного массива.

Значение этого параметра по умолчанию определяется объявлением:

#define ASC_INTERVAL_TYPE_DEFAULT aitArray

В файле с настройками ему соответствует параметр Report_IntervalType (можно указывать значение или имя элемента из перечисления).

Множитель интервала – множитель для функций расчета интервала (тип интервала aitLinear или aitPower). Для типа aitArray игнорируется. Подробности см. выше (в описании параметра «тип интервала»).

Значение этого параметра по умолчанию определяется объявлением:

#define g_cdwAscIntervalFactor 2

В файле с настройками ему соответствует параметр Report_IntervalFactor (положительное целое число).

Основание степени интервала – основание степени для функции расчета интервала (тип интервала aitPower). Для типов aitLinear и aitArray игнорируется. Подробности см. выше (в описании параметра «тип интервала»).

Значение этого параметра по умолчанию определяется объявлением:

#define g_cdwAscIntervalBase 2

В файле с настройками ему соответствует параметр Report_IntervalBase (положительное целое число).

Массив предопределенных интервалов – список значений верхних границ интервалов (тип интервала aitArray). Для типов aitLinear и aitPower игнорируется. Это одномерный массив целых положительных чисел, сортированных строго по возрастанию. Индекс в массиве соответствует индексу интервала, а значение – его верхней границе. Нижняя граница первого интервала определяется отдельным параметром (см. ниже в описании параметра «нижняя граница первого интервала предопределенного массива»).

Значение этого параметра по умолчанию определяется переменной:

static DWORD g_AscIntervalPredefinedArray[] =
  {1,2,3,5,7,10,20,50,100,200,300,400,500,600,700,800,900,1000,
    2000,5000,10000,20000,50000,100000,1000000,10000000,MAXDWORD};

В файле с настройками ему соответствует набор параметров от Report_IntervalArray_0 до Report_IntervalArray_N, где 0 и N – индексы первого и последнего элементов в предопределенном массиве. Значения всех элементов - положительные целые числа.

Нижняя граница первого интервала предопределенного массива – определяется только для типа интервала aitArray. Для типов aitLinear и aitPower игнорируется. Подробности см. выше (в описании параметра «массив предопределенных интервалов»).

Значение этого параметра по умолчанию определяется переменной:

static DWORD g_dwAscIntervalPredefinedArrayLowerBound = 0;

В файле с настройками ему соответствует параметр Report_IntervalArrayLowerBound (ноль или положительное целое число).

Отчет в одном файле – определяет, будут ли отчеты для всех встречающихся размеров блоков памяти объединены в один файл (значение параметра True), или для каждого будет создан отдельный файл (значение параметра False).

Значение этого параметра по умолчанию определяется объявлением:

#define g_bMemStatisticReportFileDefaultOnceFile TRUE

В файле с настройками ему соответствует параметр LogFile_OnceFile (тип Boolean).

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

Значение этого параметра по умолчанию определяется объявлением:

#define g_cszaMemStatisticReportFileDefaultName "ascMemStatReport"

В файле с настройками этот параметр не задается. В ascLib (при завершении работы модуля) имя отчетного файла формируется по имени модуля и передается в функцию ascMemStatisticFinish в виде параметра szaLogFileName (объявленное по умолчанию g_cszaMemStatisticReportFileDefaultName – игнорируется).

Расширение отчетного файла – только для отчета типа amsrtLogFile (для других типов игнорируется).

Значение этого параметра по умолчанию определяется объявлением:

#define g_cszaMemStatisticReportFileDefaultExtension "log"

В файле с настройками ему соответствует параметр LogFile_Extension (тип String, значение записывается без кавычек).

Каталог для отчетных файлов – полный путь до каталога. Только для отчета типа amsrtLogFile (для других типов игнорируется).

Значение этого параметра по умолчанию определяется объявлением:

#define g_cszaMemStatisticReportFileDefaultDir "C:\\TEMP\\ascMemStatisticReport"

В файле с настройками ему соответствует параметр LogFile_DirName (тип String, значение записывается без кавычек).

Разделитель для данных в отчетном файле – определяет разделитель между колонками в отчетном файле. Только для отчета типа amsrtLogFile (для других типов игнорируется).

Значение этого параметра по умолчанию определяется объявлением:

#define g_cszaMemStatisticReportFileDefaultDelimiter "\t"

В файле с настройками ему соответствует параметр LogFile_DataDelimiter (тип String, значение записывается без кавычек. Для ввода спец. символов используется слэш “\”).

Не рекомендуется изменять значение этого параметра (иначе возможны проблемы с Excel при попытке отобразить данные из полученного файла со статистикой).

Местоположение файла с настройками статистики задается определением g_cszaMemStatisticReportParamFileInDefaultFullPath. Если указать NULL, будут использоваться настройки по умолчанию. Типичное содержимое файла с настройками выглядит так:

{Файл настроек для статистики использования памяти ascMemStatictic}
{ Пояснения (см. константы в "ascMemStatistic.h"):
  Все названия параметров берутся в квадратные скобки.
  Значение параметра пишется на той же строке через разделитель (знак
табуляции или пробел). Концом значения является КОНЕЦ СТРОКИ (т.е. пробелы
и разделители допускаются!). В строке с последним параметром также должен
быть конец строки (а не конец файла).
  Если параметр пропущен или неверно задан, то берется его значение по-умолчанию.
  Параметры, являющиеся элементами массива (например, Report_IntervalArray_0)
указываются с индексом элемента в конце имени, т.е. ARRAYNAME_ELEMNIMBER, 
где ARRAYNAME - имя массива, а ELEMNIMBER - целочисленный номер элемента.
  Комментарии пишутся в фигурных скобках, могут быть многострочными, но не могут
быть вложенными. Фигурные скобки не должны использоваться нигде, кроме начала
и конца комментария.}
{*************************************************************************}
{Типы отчетов (включены/выключены)}
[Report_Overview_Enabled] True
[Report_BlockSize_Enabled] True
[Report_LoadPeak_Enabled] True
[Report_TimeInterval_Enabled] True
[Report_LogFile_Enabled] True
{Сортировка данных}
[Report_Sort] amsrstSortBySize
{Интервалы}
[Report_GroupByInterval] True
[Report_IntervalFactor] 1
[Report_IntervalBase] 2
[Report_IntervalType] aitArray
{Предопределенный массив для случая Report_IntervalType = aitArray}
{Нумерация элементов с нуля (это индексы интервалов),
значения строго по возрастанию, в значениях - UpperBound интервала}
[Report_IntervalArrayLowerBound] 0
[Report_IntervalArray_0] 1
[Report_IntervalArray_1] 3
[Report_IntervalArray_2] 6
[Report_IntervalArray_3] 10
[Report_IntervalArray_4] 15
[Report_IntervalArray_5] 20
[Report_IntervalArray_6] 50
[Report_IntervalArray_7] 100
[Report_IntervalArray_8] 200
[Report_IntervalArray_9] 300
[Report_IntervalArray_10] 400
[Report_IntervalArray_11] 500
[Report_IntervalArray_12] 600
[Report_IntervalArray_13] 700
[Report_IntervalArray_14] 800
[Report_IntervalArray_15] 900
[Report_IntervalArray_16] 1000
[Report_IntervalArray_17] 2000
[Report_IntervalArray_18] 5000
[Report_IntervalArray_19] 10000
[Report_IntervalArray_20] 20000
[Report_IntervalArray_21] 50000
[Report_IntervalArray_22] 100000
[Report_IntervalArray_23] 1000000
[Report_IntervalArray_24] 10000000
[Report_IntervalArray_25] 100000000
[Report_IntervalArray_26] 1000000000
[Report_IntervalArray_27] 4294967295 {MAXDWORD = 0xffffffff}
{Настройки для отчета типа "LogFile"}
[LogFile_Extension] log
[LogFile_DirName] C:\Temp\ascMemStatisticReport
[LogFile_DataDelimiter] \t
[LogFile_OnceFile] True
{*************************************************************************}

Полученная статистическая информация может использоваться для определения пути оптимизации работы с памятью. Оптимизировать работу с памятью можно, например, за счет создания собственного диспетчера памяти (см. статью “Quick Heap” в этом же номере RSDN).

Чтобы взять под контроль выделение памяти через Quick Heap, нужно реализовать функции OnQuickHeapPoolInternalAlloc и OnQuickHeapPoolInternalFree перед включением файла “QuickHeap.h” и объявить ASC_MEM_STATISTIC_USE_QUICK_HEAP (в ascLib это сделано в файле “ascLinInit.h”):

#define ASC_MEM_STATISTIC_USE_QUICK_HEAP
inline void OnQuickHeapPoolInternalAlloc(void * pMem, size_t size)
{
  if(pMem)
    ascMemStatisticAppend(pMem, amsaAlloc, size);
}
inline void OnQuickHeapPoolInternalFree(void * pMem)
{
  if(pMem)
    ascMemStatisticAppend(pMem, amsaFree, 0);
}
#include “QuickHeap.h”

После этого начнут отслеживаться не обращения программы к Quick Heap, а реальные занятия и освобождения памяти в heap процесса. Чтобы вернуться к контролю за обращениями к Quick Heap (то есть вновь получить графики, представленные на рис.1), достаточно закомментировать объявление ASC_MEM_STATISTIC_USE_QUICK_HEAP.

После подключения Quick Heap статистика использования памяти выглядит иначе, чем без него. График работы с памятью для Test001.dll (рис.3) существенно отличается от показанного на рисунке 1:


Рисунок 3. Статистика использования памяти в библиотеке Test001.dll после подключения Quick Heap.

На рисунке 3 видно, что теперь память освобождается только при завершении программы, в ходе же ее выполнения – только занимается (блоки размером 16 байт занимаются Quick Heap для внутренних нужд).

Аналогичным образом изменяется картина работы с памятью и в примере с библиотекой ascDbShared.dll (рисунок 4):


Рисунок 4. Статистика использования памяти в библиотеке ascDbShared.dll после подключения Quick Heap.

Теперь (с использованием Quick Heap) общее количество занятых блоков сократилось с 30405 до 189. Одновременно примерно вдвое возрос объем занятой памяти (если сравнивать пиковые загрузки). Это можно увидеть в debug window (тип отчета amsrtOverview). Ниже приведен фрагмент отчета:

С подключенным Quick Heap:
Full count: 189;   Full size: 1489906;   Average size: 7883  Max. peak size: 1082034
Без Quick Heap:
Full count: 30405;   Full size: 2593970;   Average size: 85   Max. peak size: 627792

Как видно из отчета, за счет использования всего лишь вдвое (точнее, в 1.7 раз) большего объема памяти количество запросов к системе на занятие и освобождение памяти снизилось в 160 раз. В данном конкретном случае это не привело к заметному ускорению работы программы – основное время в ней расходуется не на занятия памяти. Следовательно, в этой программе нет потребности в оптимизации доступа к памяти.

Однако, для программ, расходующих время в основном на работу с памятью, оптимизация доступа к памяти может оказаться существенной. В тестовом примере, занимающемся исключительно занятием и освобождением блоков памяти одного размера, с помощью Quick Heap удалось поднять производительность в 2.5 раза. Этот пример был скомпилирован на VC6 в конфигурации ReleaseMinSize (включена оптимизация по скорости). Ниже приведен основной код примера:

const int iCount = 1000000;
const int iSize = 100;
void * arr[iCount];
//...
DWORD dwTickCount = GetTickCount();
int i;
for(i = 0; i < iCount; ++i)
{
  arr[i] = malloc(iSize);
}
for(i = 0; i < iCount; ++i)
{
  free(arr[i]);
}
dwTickCount = GetTickCount() - dwTickCount;

Результаты этого теста показаны ниже (в первой колонке – количество и размер блоков в байтах, во второй – время выполения теста):

ascLib отключен:

с 1000000 по 100 - 1440 мс

с 100000 по 1000 - 250 мс

с 10000 по 10000 - 70 мс

ascLib подключен без QuickHeap:

с 1000000 по 100 - 1280 мс

с 100000 по 1000 - 245 мс

с 10000 по 10000 - 70 мс

ascLib подключен с QuickHeap:

с 1000000 по 100 - 655 мс

с 100000 по 1000 - 100 мс

с 10000 по 10000 - 70 мс

Бросается в глаза разница между временем занятия блоков размера 100 и 1000 байт для вариантов с Quick Heap и без него (примерно 2.5 раза). Также заметно, что для блоков размером 10000 байт разницы нет. Это связано с тем, что Quick Heap по умолчанию готовится к оптимизации повторного доступа к блокам памяти, размером до 1024 байт. Если программа по каким-то причинам многократно занимает память более значительными кусками, можно изменить настройки Quick Heap в реестре (ключ HKEY_LOCAL_MACHINE\SOFTWARE\optim.su\ QuickHeap). Значение QHInitPoolArraySize отвечает за размер массива (оно же определяет максимальный размер блоков, для которых будет создаваться pool). Значение QHInitPoolSize определяет размер pool’а при его создании (по умолчанию 4096). Значение QHInitPoolSize обязательно должно быть больше, чем QHInitPoolArraySize (это ограничение связано с особенностями реализации Quick Heap).

Увеличение QHInitPoolArraySize до 10001 и QHInitPoolSize до 10002 в вышеупомянутом тесте приводит к уменьшению времени на занятия 10000 блоков по 10000 байт до 30 мс (вместо 70 мс раньше). Более подробно об использовании Quick Heap сказано в одноименной статье («Quick Heap») в этом же номере журнала.

Итак, получение статистики может быть очень полезно при оптимизации доступа к памяти. Одна из таких возможностей (настройка размера массива в Quick Heap) была продемонстрирована выше. Разумеется, можно написать свой собственный контроль памяти, но проще всего для этой цели воспользоваться статистикой, реализованной в ascLib.


Эта статья опубликована в журнале RSDN Magazine #1. Информацию о журнале можно найти здесь