Consumo abusivo de memória

Consumo abusivo de memória

Era um belo dia em um ambiente de processamento fictício de filas fictícias e threads fictícias. Eis um belo código com filas, threads e processamentos feitos em stop-motion:

#include <windows.h> // critical section, create thread...
#include <list> // nossa lista interna
#include <time.h> // randomiza??o
 
struct Queue // uma fila (duh)
{
    size_t bufferSize; // cada item ? um buffer de tamanho fixo
    DWORD wait; // antes de processar, aguardemos esse tempo fixo
    CRITICAL_SECTION cs; // stl ? thread-safe, pero no mucho
    std::list<char*> items; // os itens!
};
 
DWORD WINAPI InsertItems(LPVOID pvQueue) // insere, insere, insere....
{
    Queue& queue = *(Queue*) pvQueue;
    for( int i = 0; i < 10 * 1000; ++i ) // 10k itens!
    {
        char* buffer = new char[queue.bufferSize];
        memset(buffer, (int) (i % ('Z' - 'A')) + 'A', queue.bufferSize); // teoricamente de A a Z
        buffer[queue.bufferSize - 1] = 0; // string C pra facilitar nossa depura??o
        EnterCriticalSection(&queue.cs); // deixa eu entrar!
        queue.items.push_back(buffer);
        LeaveCriticalSection(&queue.cs); // deixa eu sair!
        Sleep(10); // d? uma dormidinha (sempre menor dormidinhas do processamento)
    }
    return ERROR_SUCCESS; // "t? tudo certo!" (by Starcraft 2)
}
 
DWORD WINAPI ProcessItems(LPVOID pvQueue) // processa, processa, processa...
{
    Queue& queue = *(Queue*) pvQueue;
    DWORD wait = 2;
    Sleep(10000); // como um advogado oportunista, aguardamos por algu?m pra processar
    while( ! queue.items.empty() ) // agora vai at? esvaziar o recinto
    {
        EnterCriticalSection(&queue.cs); // deixa eu entrar!
        char* buffer = queue.items.front();
        queue.items.pop_front();
        LeaveCriticalSection(&queue.cs); // deixa eu sair!
        delete [] buffer;
        Sleep(queue.wait); // aguarda por... por quanto mesmo?
    }
    return ERROR_SUCCESS; // "t? tudo certo!" (by Starcraft 2)
}
 
int main(int argc, char* argv[]) // No princ?pio havia a pilha, quando Deus disse: 'int main!'
{
    static const size_t QUEUES_SIZE = 20; // n?mero de filas sendo processadas
    static const size_t QUEUE_ITEM_SIZE = 0x1000; // 1KB ? o chunk alocado por item
    static const DWORD WAIT_TIMES[] = { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 1000 }; // algu?m vai esperar demais
 
    Queue queues[QUEUES_SIZE]; // as filas
    HANDLE queueThreads[QUEUES_SIZE * 2]; // as threads que processam as filas
 
    srand((unsigned int)time(0)); // randomizemos tudo
 
    for( size_t i = 0; i < QUEUES_SIZE; ++i )
    {
        queues[i].bufferSize = QUEUE_ITEM_SIZE + i; // para diferenciarmos as filas
        queues[i].wait = WAIT_TIMES[ rand() % (sizeof(WAIT_TIMES) / sizeof(DWORD)) ]; // vamos esperar por... por quanto mesmo?
        InitializeCriticalSection(&queues[i].cs); // deu crash em algumas situa?es em release (stl deveria ser thread-safe...)
        queueThreads[i] = CreateThread(NULL, 0, InsertItems, &queues[i], 0, NULL); // criamos thread de inser??o
        queueThreads[QUEUES_SIZE + i] = CreateThread(NULL, 0, ProcessItems, &queues[i], 0, NULL); // criamos thread de processamento
    }
 
    WaitForMultipleObjects(QUEUES_SIZE * 2, queueThreads, TRUE, INFINITE); // espera a 'gaguera'
    return 0; // "t? tudo certo!" (by Starcraft 2)
}

Se olharmos de perto o processamento e a memória consumida por esse processo, veremos que no início existe um boom de ambos, mas após um momento de pico, o processamento praticamente pára, mas a memória se mantém:

Depois de pesquisar por meus tweets favoritos, fica fácil ter a receita para verificarmos isso usando nosso depurador favorito: Visual Studio WinDbg!

windbg -pn MemoryConsumption.exe

Achamos onde está a memória consumida. Agora precisamos de dicas do que pode estar consumindo essa memória. Vamos começar por listar os chunks alocados por tamanho de alocação:

0:004> !heap -stat -h 0
Allocations statistics for
 heap @ 00670000
group-by: TOTSIZE max-display: 20
 size #blocks total ( %) (percent of total busy bytes)
 1037 25e5 - 2667433 (33.04)
 1025 25e6 - 263da3e (32.90)
 1024 25e4 - 2639410 (32.89)
...

O Top 3 é de tamanhos conhecidos pelo código, de 1024 a 1024 + QUEUES_SIZE – 1. O de tamanho 1037, por exemplo, possui 0x25e5 blocos alocados. Vamos listar cada um deles:

0:004> !heap -flt s 1037
 _HEAP @ 420000
 _HEAP @ 670000
 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
 00558600 0221 0000 [00] 00558618 01037 - (busy)         <--- vamos usar esse primeiro mais tarde
 0055fd38 0221 0221 [00] 0055fd50 01037 - (busy)
 00561f48 0221 0221 [00] 00561f60 01037 - (busy)
 00565260 0221 0221 [00] 00565278 01037 - (busy)
 0056c998 0221 0221 [00] 0056c9b0 01037 - (busy)
 0056daa0 0221 0221 [00] 0056dab8 01037 - (busy)
 0056eba8 0221 0221 [00] 0056ebc0 01037 - (busy)
 00570db8 0221 0221 [00] 00570dd0 01037 - (busy)
 00572fc8 0221 0221 [00] 00572fe0 01037 - (busy)
 005740d0 0221 0221 [00] 005740e8 01037 - (busy)
 0058abc8 0221 0221 [00] 0058abe0 01037 - (busy)
 00595618 0221 0221 [00] 00595630 01037 - (busy)
 00599a38 0221 0221 [00] 00599a50 01037 - (busy)
 0059de58 0(...)

A listagem do depurador nos dá o endereço onde o chunk foi alocado no heap e o endereço devolvido para o usuário, onde colocamos nossas tralhas. Através de ambos é possível trackear a pilha da chamada que alocou cada pedaço de memória. Isso, claro, se previamente tivermos habilitado essa informação através do GFlags:

0:004> !heap -p -a 00558600
 address 00558600 found in
 _HEAP @ 670000
 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
 00558600 0221 0000 [00] 00558618 01037 - (busy)
 Trace: b7a24
 7722dfa2 ntdll!RtlAllocateHeap+0x00000274
 5b628343 MSVCR100D!_heap_alloc_base+0x00000053
 5b63697c MSVCR100D!_nh_malloc_dbg+0x000002dc
 5b63671f MSVCR100D!_nh_malloc_dbg+0x0000007f
 5b6366cc MSVCR100D!_nh_malloc_dbg+0x0000002c
 5b639c5b MSVCR100D!malloc+0x0000001b
 5b627db1 MSVCR100D!operator new+0x00000011
 e84dee MemoryConsumption!operator new[]+0x0000000e
 e818be MemoryConsumption!InsertItems+0x0000004e
 7679339a kernel32!BaseThreadInitThunk+0x0000000e
 771e9ef2 ntdll!__RtlUserThreadStart+0x00000070
 771e9ec5 ntdll!_RtlUserThreadStart+0x0000001b

 

Dessa forma temos onde cada memória foi alocada, o que nos dará uma informação valiosa, dependendo qual o tipo de problema estamos tentando resolver.

0:004> u e818be
MemoryConsumption!InsertItems+0x4e [c:\...\memoryconsumption.cpp @ 18]:
00e818be 83c404 add esp,4
00e818c1 898514ffffff mov dword ptr [ebp-0ECh],eax
00e818c7 8b9514ffffff mov edx,dword ptr [ebp-0ECh]
00e818cd 8955e0 mov dword ptr [ebp-20h],edx
00e818d0 8b45f8 mov eax,dword ptr [ebp-8]
00e818d3 8b08 mov ecx,dword ptr [eax]
00e818d5 51 push ecx
00e818d6 8b45ec mov eax,dword ptr [ebp-14h]

Outra informação relevante é o que está gravado na memória, que pode nos dar insights de que tipo de objeto estamos lidando:

0:004> db 00558618
00558618 c0 b7 8c 0b 98 03 55 00-00 00 00 00 00 00 00 00 ......U.........
00558628 13 10 00 00 01 00 00 00-15 94 00 00 fd fd fd fd ................
00558638 51 51 51 51 51 51 51 51-51 51 51 51 51 51 51 51 QQQQQQQQQQQQQQQQ
00558648 51 51 51 51 51 51 51 51-51 51 51 51 51 51 51 51 QQQQQQQQQQQQQQQQ
00558658 51 51 51 51 51 51 51 51-51 51 51 51 51 51 51 51 QQQQQQQQQQQQQQQQ
00558668 51 51 51 51 51 51 51 51-51 51 51 51 51 51 51 51 QQQQQQQQQQQQQQQQ
00558678 51 51 51 51 51 51 51 51-51 51 51 51 51 51 51 51 QQQQQQQQQQQQQQQQ
00558688 51 51 51 51 51 51 51 51-51 51 51 51 51 51 51 51 QQQQQQQQQQQQQQQQ

Não é o caso, mas vamos supor que fosse um objeto/tipo conhecido. Poderíamos simplesmente “importar” o tipo diretamente do PDB que estamos para modelar a memória que encontramos em volta. Mais detalhes em outro artigo.

Funções/classes usadas nesse artigo

  • CreateThread. Cria uma nova linha de execução.
  • WaitForMultipleObjects. Pode aguardar diferentes linhas de execução terminarem.
  • std::list. Lista na STL para inserir/remover objetos na frente e atrás (ui).
  • Initialize, Enter e LeaveCriticalSection. Uma maneira simples de criar blocos de entrada atômica (apenas uma thread entra por vez).
  • memset. Se você não sabe usar memset, provavelmente não entendeu nada desse artigo.
316 Visitas no Total 3 Visitas Hoje