Rodrigo Strauss :: Blog
Usando Win32 API para otimizar o I/O, parte 3
Parte Zero Parte 1 Parte 2 Parte 3 Parte 4 Parte 5 Parte 6 Fontes do parser
Agora que já tivemos bastante bla-bla-bla e medições, está na hora de fazer alguma otimização. A primeira otimização que faremos consiste em mudar os acessos aos arquivos - que hoje são feitos usando a runtime do C++ - para usar a Win32 API diretamente. Duas modificações serão feitas:
- Usar CreateFile/ReadFile para ler o conteúdo do arquivo;
- Usar VirtualAlloc para alocar memória ao invés de alocar do heap (que é o que a std::string faz).
Como um trecho de código vale sempre mais do que ~340 m/s, vou mostrá-lo antes e explicar depois:
void MidlParser::ParseMidlFile(const char* fileName) { HANDLE hFile; char* szFileContent; DWORD dwFileSize; DWORD dwReaded; if(m_bParseAsImportFile) { // // vamos procurar o arquivos nas pasta determinadas para include // for(vector::const_iterator i = m_IncludePaths.begin() ; i != m_IncludePaths.end() ; ++i) { string str = *i + fileName; hFile = CreateFile(str.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL); if(hFile != INVALID_HANDLE_VALUE) break; } if(hFile == INVALID_HANDLE_VALUE) throw ParseException(string("error opening import file \"") + fileName + "\"", *this, m_parsedFileName); dwFileSize = GetFileSize(hFile, NULL); // // ao invés de alocar do Heap (que é para alocações pequenas), // vou alocar direto da memória virtual. O Heap Manager usa // memória virtual para isso, então estaremos // "pulando" um nível de abstração. // //szFileContent = (char*)HeapAlloc(GetProcessHeap(), NULL, dwFileSize); szFileContent = (char*)VirtualAlloc(NULL, dwFileSize + (dwFileSize % 4096), MEM_COMMIT, PAGE_READWRITE); // // vamos ler o arquivo inteiro de uma vez // ReadFile(hFile, szFileContent, dwFileSize, &dwReaded, NULL); ... } else { hFile.Create(fileName, GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING); if(hFile.m_h == INVALID_HANDLE_VALUE) throw ParseException(string("error opening file \"") + fileName + "\"", *this, m_parsedFileName); dwFileSize = GetFileSize(hFile, NULL); //szFileContent = (char*)HeapAlloc(GetProcessHeap(), NULL, dwFileSize); szFileContent = (char*)VirtualAlloc(NULL, dwFileSize + (dwFileSize % 4096), MEM_COMMIT, PAGE_READWRITE); ReadFile(hFile, szFileContent, dwFileSize, &dwReaded, NULL); ... }
Algumas coisas importantes devem ser notadas nessa implementação. A primeira é que, além de mudar o acesso para usar CreateFile/ReadFile, eu pego antes o tamanho do arquivo, para ler tudo de uma vez. Não sabemos (por enquanto) como a runtime do C++ faz a leitura, mas ler o arquivo todo de uma vez só é a melhor forma. Outro detalhe da implementação é que eu uso VirtualAlloc para alocar memória e não HeapAlloc/new/malloc.
Antes de explicar o motivo de usar VirtualAlloc, uma explicação sobre gerenciamento de memória no Windows. A função mais low-level para alocar memória em user mode é a VirtualAlloc. Ela aloca memória no endereçamento do processo, com a granulação de uma página de memória (no Windows, em user mode, e em arquitetura x86, uma página de memória é de 4kb). Sendo assim, só podemos alocar múltipos de uma página. Por isso eu coloquei um dwFileSize + (dwFileSize % 4096) no tamanho, para arredondar a quantidade alocada para o primeiro múltiplo de 4kb maior do que a quantidade necessária.
Como muitas vezes os programas precisam de áreas de memória menores do que 4kb, usar no mínimo esse valor em cada alocação seria um desperdício muito grande. Por isso existem os heaps, que são pools de memória que servem alocações em granulações menores. Apesar de (na maioria das vezes) reduzir o desperdício de memória, o heap é mais lento do que o VirtualAlloc, porque uma alocação envolve algorimos de procura de blocos livres e desfragmentação da memória alocada para o heap.
O Windows possui gerenciamento de heap nativo, tanto em user mode quanto em kernel mode. Em user mode a função que aloca memória de um heap é a HeapAlloc(HANDLE,DWORD,SIZE_T), onde o primeiro parâmetro é um HANDLE para um heap. Você pode usar o heap default do processo (chamando GetProcessHeap()) ou criar um heap usando HeapCreate(). A runtime do C/C++ usa o HeapAlloc, mas cria um heap separado do heap padrão do processo.
Usei o VirtualAlloc porque ele é mais rápido do que o heap (já que aloca uma página diretamente e não tem problemas de fragmentação) e não envolve mais esforço de programação. Além disso, o desperdício da memória que ficará entre o que usamos e o fim da página será praticamente desprezível. Leremos aproximadamente 400kb, ou seja, 100 páginas. Mesmo se 100% das páginas forem desperdiçadas (o que é impossível acontecer), estaríamos usando 400kb a mais. Eu medi o desperdício para os arquivos do SDK, e ele é de 5,21% (o que é realmente desprezzzzível, como diria o Patolino).
Voltando à otimização de I/O, aqui está a medição que eu fiz depois dessas otimizações:
Média de tempo (50 chamadas):
228,01 ms - versão usando a runtime do C++
214,68 ms - versão usando Win32 API
Melhoria: 6,21%
Para primeira tentativa de otimização, 6,21% não é tão mal... Na próxima parte veremos se podemos fazer melhor.
Em 17/08/2005 23:07, por Rodrigo Strauss





Nessa altura do campeonato as idéias começam a borbulhar, e a vontade de estragar as surpresas que poderão vir nas próximas partes é grande. Contudo, como você não acabou, nada mais justo do que aguardar a evolução gradual do seu processo de otimização _consciente_, _localizado_ e _estatisticamente_ _comprovado_, coisas essenciais que fazem valer a pena o processo de otimizar o código, e não famigerados clichês como usar aritmética de ponteiro ao invés de subscrito etc. Mas, voltando, melhor ver as idéias antes de dar uma de "parpiteiro" de "prantão".