logo
Contato | Sobre...        
rebarba rebarba

Rodrigo Strauss :: Blog

follow us in feedly

Usando Win32 API para otimizar o I/O, parte 6

Parte Zero     Parte 1     Parte 2     Parte 3     Parte 4     Parte 5     Fontes do parser

Depois de turbulências, mudanças de endereço e besteiras, é hora de dar continuidade à série sobre otimização de desempenho de I/O. Nos nossos exemplos, eu usei um interpretador de IDL que deve ler o conteúdo do arquivo em questão e vários imports, para mostrar o grau de otimização que é possível usando a API Win32 ao invés da runtime do C++.

Só para esclarecer: a runtime do C++ não é lenta, é até que bastante rápida. O grande negócio é que qualquer abstração deixa o código mais lento, já que são mais camadas intermediárias para fazer o que realmente se quer, e mais código será rodado para fazer a mesma coisa. É esse o motivo pelo qual, na esmagadora maioria das vezes, um programa feito em Visual C++ é bem mais rápido do que um feito em .NET ou Java. Além de abstrair a API nativa do sistema operacional - que a runtime do C++ também faz - o Java e o .NET abstraem também a arquitetura do computador onde o programa está rodando. A API nativa do Windows é a Win32, qualquer coisa que não use a API diretamente (VB6, Delphi, Java, .NET, [coloque-algo-que-não-seja-C-e-C++-puros-aqui] fica sempre mais lento do que usar a API diretamente. O fato é que as vezes a diferença é muito pequena (como no caso do Delphi) e não vale a pena o trabalho de usar Win32 (que não é pequeno). Na realidade, hoje todo mundo usa alguma abstração sobre Win32, seja ela uma grande abstração (.NET) ou uma pequena abstração (MFC).

Nessa nossa otimização, iremos usar um recurso do Windows chamado File Mapping. Esse recurso permite mapear um trecho de um arquivo diretamente na memória, e a medida que a memória é lida, o conteúdo do arquivo é colocado nela automaticamente. Para o programa, é como se o conteúdo do arquivo já estivesse na memória, mas na realidade o arquivo é lido a medida que o programa acessa essa memória. Isso evita buffers intermediários, e no final das contas, é mais simples do que alocar memória, ler o arquivo e depois tratar.

O File Mapping é implementado em kernel mode como um recurso chamado Section. Quando você mapeia um trecho do arquivo na memória (usando MapViewOfFile em user mode), o arquivo não é lido para memória na hora. As páginas de memória são marcadas como inválidas, o que faz com o que sejá disparada uma exceção quando essa memória é acessada. Essa exceção é tratada pelo Memory Manager, que faz a leitura do trecho acessado do arquivo para essa memória, marca a página como válida e retorna o controle para o programa, que agora terá o conteúdo esperado do arquivo nessa memória. O File Mapping também pode ser usado para compartilhar memória entre os programas. O Windows usa esse recurso para carregar executáveis e DLLs na memória, o que faz com que o conteúdo de uma DLL só seja carregado uma vez para todos os processos que estão usando-a.

Para simplificar o uso do File Mapping no nosso interpretador e para isolar um pouco o código Win32 do nosso código, criei uma classe simples para tratar o File Mapping:

//
// Classe que mapeia um arquivo inteiro na memória
// usando FileMapping
//
// Rodrigo Strauss - http://www.1bit.com.br
//
class Win32FileMapping
{
   HANDLE m_hFile, m_hFileMapping;
   DWORD m_dwFileSize;
   void* m_p;

   void Clean()
   {
      m_hFileMapping = NULL;
      m_hFile = INVALID_HANDLE_VALUE;
      m_p = NULL;
      m_dwFileSize = 0;
   }

public:
   Win32FileMapping()
   {
      Clean();
   }

   ~Win32FileMapping()
   {
      Free();
   }

   bool IsValid()
   {
      return m_p && m_hFileMapping && m_hFile != INVALID_HANDLE_VALUE;
   }

   void Free()
   {
      if(m_p)
         UnmapViewOfFile(m_p);

      if(m_hFileMapping)
         CloseHandle(m_hFileMapping);

      if(m_hFile != INVALID_HANDLE_VALUE)
         CloseHandle(m_hFile);

      Clean();
   }

   //
   // Essa função mapeia o arquivo inteiro na memória
   //
   bool MapFile(const char* fileName)
   {
      Free();

      m_hFile = CreateFile(fileName, 
                           GENERIC_READ, 
                           FILE_SHARE_READ, 
                           NULL, 
                           OPEN_EXISTING, 
                           NULL, 
                           NULL);

      if(m_hFile == INVALID_HANDLE_VALUE)
         return false;

      m_dwFileSize = GetFileSize(m_hFile, NULL);

      m_hFileMapping = CreateFileMapping(m_hFile, 
                                         NULL,
                                         PAGE_READONLY, 
                                         0,
                                         0, 
                                         NULL);

      if(m_hFileMapping == NULL)
    {
     Free();
         return false;
    }

      m_p = MapViewOfFile(m_hFileMapping, FILE_MAP_READ, 0, 0, 0);

      return true;
   }

   template<typename T>
   T GetStart()
   {
      return reinterpret_cast<T>(m_p);
   }

   template<typename T>
   T GetEnd()
   {
      if(!m_dwFileSize)
         return NULL;

      return reinterpret_cast<T>( ((unsigned char*)m_p) + m_dwFileSize );
   }
};

Essa classe mapeia o arquivo inteiro na memória, e retorna os ponteiros de início de de fim através das funções template GetStart() e GetEnd(). Note que o GetEnd() retorna um ponteiro para o primeiro T depois do fim do arquivo. Esse o mesmo conceito usado pelos containers STL, onde o end() retorna um elemento inválido após o último item. O último item de um container é [container.end() - 1] (se, e somente se container.begin() != container.end())

O boost::tokenizer tem duas opções de inicialização: passar uma string com o conteúdo a ser interpretado, ou um iterator inicial e um iterator final. No nosso caso, o iterator inicial é GetStart() e o final GetEnd(). Não se esqueça que em STL, o conceito iterator é uma generalização de um ponteiro: um objeto que aponte para outro objeto (ou seja, sobrecarregue o operador *) e que (não necessariamente) possa ser incrementado e decrementado (operadores ++ e --) para acessar outros itens (para mais detalhes veja a documentação no site da SGI). Então um ponteiro É um iterator, o que resolve o nosso problema de uma forma bem simples.

Vamos ao código (é por isso que você está lendo isso, não é?):

void MidlParser::ParseMidlFile(const char* fileName)
{
   ...
   Win32FileMapping fileMapping;


   if(m_bParseAsImportFile)
   {
      //
      // tenta abrir o imported file nas pastas de include
      //
    for(vector<string>::const_iterator i = m_IncludePaths.begin() ;
            i != m_IncludePaths.end() ; 
            ++i)
      {
         string str;

         str = *i + fileName;

         fileMapping.MapFile(str.c_str());

         if(fileMapping.IsValid())
            break;
      }

      if(!fileMapping.IsValid())
         throw ParseException(string("error opening import file \"") 
       + fileName + "\"", *this, m_parsedFileName);

      //
      // inicializa o tokenizer com os ponteiros do FileMapping
      //
      m_Tokenizer.assign(fileMapping.GetStart<char*>(),
                         fileMapping.GetEnd<char*>(),
                         char_separator<char>("\t\r " , "\n\"*,;:{}/\[]()"));


      m_ParsedIncludeFiles.push_back(fileName);

   }
   else
   {
      fileMapping.MapFile(fileName);
      
      if(!fileMapping.IsValid())
         throw ParseException(string("error opening file \"") 
       + fileName + "\"", *this, m_parsedFileName);

      //
      // inicializa o tokenizer com os ponteiros do FileMapping
      //
      m_Tokenizer.assign(fileMapping.GetStart<char*>(),
         fileMapping.GetEnd<char*>(),
         char_separator<char>("\t\r " , "\n\"*,;:{}/\[]()"));

      m_parsedFileName = fileName;

      ...
   }
}

Como visto, nosso código não ficou mais complicado por causa do File Mapping. Nossa classe facilitou muito e deixou o código claro, além da inteligente decisão de fazer uma classe simples que resolvesse nosso problema pontual ao invés de tentar fazer um framework genérico para uso avançado de File Mapping. Um péssimo costume de "programadores orientados à objetos" é fazer classes super genéricas que resolvam todas as situações possíveis e imagináveis naquela área (eu sei bem porque eu era assim...).

Chega de yada-yada, vamos aos números:

Média de tempo (50 chamadas):

228,01 ms - versão usando a runtime do C++
214,68 ms - versão usando CreateFile
204,17 ms - versão usando CreateFile com FILE_FLAG_SEQUENTIAL_SCAN
186,55 ms - versão usando File Mapping

Comparação entre a implementação com File Mapping e as anteriores
22,22% - Melhoria em relação versão que usa a runtime do C++ (versão inicial)
15,08% - Melhoria em relação versão que usa CreateFile
09,44% - Melhoria em relação versão que usa CreateFile com FILE_FLAG_SEQUENTIAL_SCAN

Nossa melhoria em relação à versão inicial foi de 22%, e as mudanças foram pequenas e pontuais. Nessa última tentativa, fizemos a classe de File Mapping (menos de 90 linhas de código e anos de estudo de C++ e Win32) e modificamos o código que carrega o arquivo (menos de 10 linhas de código e mais de 10 livros de programação depois). O nosso programa de exemplo não é tão I/O intensive, mas nossa otimização melhorou bastante o desempenho em termos pencentuais.

Mesmo assim, há algo para se pensar: será que valeu a pena? Essa série foi interessante para ilustrar conceitos de otimização de performance e Win32, mas agora chega a hora de explicar mais um conceito: comparação percentual não é suficiente para fazer um estudo sobre uma determinada otimização. 22% é uma otimização considerável, mas e em termos absolutos? Será que 41,46 milisegundos é algo que faça diferença no tempo de compilação? Nosso caso o esforço de programação foi pequeno (o de escrever os posts foi imensamente maior), mas mesmo assim deve ser levado em consideração.

Quando for mensurar alguma otimização, considere TODOS os parâmetros, inclusive os intangíveis, como o impacto que essa otimização causará no usuário. No caso do nosso interpretador, talvez ~40ms não faça tanta diferença. Mas em uma chamada de componente que roda no backend de uma bolsa de valores e que é chamado milhões de vezes em um único dia, 5ms faz uma diferença MUITO grande.

Lembre-se: programação é uma arte.


Em 27/09/2005 15:44, por Rodrigo Strauss


  
 
 
Comentários
Alexsabdro | website | em 04/09/2007 | #
Olá, é a primeira vez que vejo seus artigos e gostaria de saber se você pode postar todos esles, desde o primeiro até o ultimo. Este é a parte 6 se poder postar para o e-mail sistemaufpa@yahoo.com.br eu agradeço.
Desde já obrigado!
Rodrigo Strauss | website | em 04/09/2007 | #
Na parte de cima do post, bem embaixo do título estão os links para todos os posts da série.
Algo a dizer?
Nome:


Site:


E-mail:


Escreva o número vinte e seis:


 Não mostre meu e-mail no site, não serve pra nada mesmo...

Comentário





Os comentários devem ser sobre assuntos relativos ao post, eu provavelmente apagarei comentários totalmente offtopic. Se quiser me enviar uma mensagem, use o formulário de contato. E não esqueça: isso é um site pessoal e eu me reservo o direito de apagar qualquer comentário ofensivo ou inapropriado.
rebarba rebarba
  ::::