Compartilhar via


Dicas para melhorar código crítico em termos de tempo

Para gravar códigos rápidos, você precisa conhecer todos os aspectos do aplicativo e saber como ele interage com o sistema. Este artigo sugere alternativas para algumas das técnicas de codificação mais óbvias que podem ajudá-lo a garantir que as partes do código, para as quais o tempo é essencial, tenham um desempenho satisfatório.

Em suma, para melhorar o código para o qual o tempo é essencial, você precisa:

  • Saber quais partes do programa precisam ser rápidas.

  • Saiba o tamanho e a velocidade do seu código.

  • Saiba o custo dos novos recursos.

  • Conhecer o trabalho mínimo necessário para concluir a tarefa.

Para coletar informações sobre o desempenho do seu código, você pode usar o monitor de desempenho (perfmon.exe).

Seções deste artigo

Perdas no cache e falhas de página

A ocorrência de falhas de cache nos caches interno e externo, assim como as falhas de página (direcionamento para o armazenamento secundário das instruções e dos dados do programa), tornam o desempenho do programa mais lento.

Uma ocorrência no cache da CPU pode custar ao programa de 10 a 20 ciclos de relógio. Um acesso ao cache externo pode custar de 20 a 40 ciclos de relógio. Uma falha de página pode custar um milhão de ciclos (partindo do princípio de que o processador gerencia 500 milhões de instruções por segundo e que a falha leva 2 milissegundos). Portanto, o melhor para a execução do programa é que você escreva um código que diminua as falhas de cache e page faults.

Uma das causas da lentidão dos programas é que esses programas têm mais falhas de página ou mais perda no cache com mais frequência do que o necessário. Para evitar esse problema, é importante usar estruturas de dados com boa localidade de referência, o que significa manter itens relacionados juntos. Às vezes, uma estrutura de dados, que parece excelente, acaba sendo horrível devido à má localidade de referência, e o contrário também pode acontecer. Veja dois exemplos:

  • Listas vinculadas com alocação dinâmica podem prejudicar o desempenho do programa. Quando você pesquisa um item ou navega até o final da lista, cada link ignorado pode sofrer perda no cache ou causar uma falha de página. Uma implementação da lista baseada em arrays simples pode ser mais rápida devido ao melhor uso de cache e a menos falhas de página. Mesmo se você levar em consideração o fato de que a matriz seria mais difícil de expandir, ela ainda pode ser mais rápida.

  • As tabelas de hash que usam listas vinculadas com alocação dinâmica podem prejudicar o desempenho. Tabelas de hash que usam listas ligadas alocadas dinamicamente para armazenar seu conteúdo podem apresentar desempenho substancialmente pior. Na verdade, na análise final, uma simples pesquisa linear na matriz pode ser mais rápida (dependendo das circunstâncias). O uso de uma tabela de hash baseada em vetores (também conhecida como "hash fechado") é uma implementação frequentemente ignorada, mas frequentemente possui desempenho superior.

Classificação e pesquisa

Por natureza, a classificação consome mais tempo do que muitas operações comuns. A melhor maneira de evitar a lentidão desnecessária é evitar ordenar em momentos críticos. Você também pode:

  • Adiar a classificação até um momento em que o desempenho não seja crítico.

  • Classificar os dados em um tempo crítico anterior, não relacionado ao desempenho.

  • Classificar apenas os dados que realmente precisam ser classificados.

Em alguns casos, você também pode compilar a lista na ordem classificada. Cuidado. Se precisar inserir dados na ordem classificada, você pode precisar de uma estrutura de dados complexa com má localidade de referência, o que resulta em perdas no cache e falhas de páginas. Não existe uma única solução para todos os casos. Experimente diversas possibilidades e avalie as diferenças.

Veja algumas dicas gerais referentes à classificação:

  • Use a classificação de inventário para minimizar os bugs.

  • Qualquer trabalho que você puder fazer previamente para diminuir a complexidade da classificação é vantajoso. Uma única passagem sobre os seus dados, se simplificar as comparações e reduzir a ordenação de O(n log n) para O(n), você quase certamente estará em vantagem.

  • Pense na localidade de referência do algoritmo de classificação e nos dados em que ela deve ser executada.

Há menos alternativas para as pesquisas do que para a classificação. Se o tempo for crítico na operação de pesquisa, uma pesquisa binária ou verificação da tabela de hash quase sempre é a melhor opção, mas no caso da classificação, é necessário levar a localidade em consideração. Uma pesquisa linear por meio de uma pequena matriz pode ser mais rápida do que uma pesquisa binária por meio de uma estrutura de dados com muitos ponteiros, que resultam em falhas na página ou perdas no cache.

MFC e bibliotecas de classe

As MFC (Microsoft Foundation Classes) podem simplificar muito a escrita de código. Ao gravar códigos para os quais o tempo é crítico, você deve levar em consideração a sobrecarga inerente a algumas dessas classes. Avalie o código da MFC que seu código com tempo crítico usa para ver se ele atende às suas necessidades de desempenho. A lista a seguir identifica as classes e as funções de MFC que você deve conhecer:

  • CString MFC chama a biblioteca de runtime C para alocar memória para um CString dinamicamente. Em termos gerais, CString é tão eficiente quanto qualquer outra cadeia de caracteres com alocação dinâmica. Assim como qualquer string alocada dinamicamente, ela tem a sobrecarga de alocação e liberação dinâmicas. Geralmente, um array simples char na pilha pode cumprir a mesma função e ser mais rápido. Não use um CString para armazenar uma cadeia de caracteres constante. Use o const char * em vez disso. Qualquer operação executada com um objeto CString acarreta alguma sobrecarga. Pode ser mais rápido usar funções de cadeia de caracteres de biblioteca de runtime.

  • CArray Um CArray fornece flexibilidade que uma matriz regular não oferece, mas talvez o programa não precise disso. Se conhecer os limites específicos da matriz, você pode usar uma matriz global fixa. Se você usar CArray, use CArray::SetSize para definir o seu tamanho e especificar em quantos elementos ele cresce quando uma realocação é necessária. Caso contrário, a adição de elementos pode fazer com que a matriz seja realocada e copiada com frequência, o que é ineficaz e pode fragmentar a memória. Além disso, se você inserir um item em uma matriz, CArray moverá os itens subsequentes na memória e poderá precisar expandir a matriz. Essas ações podem resultar em perdas no cache e falhas de página. Se você verificar o código usado pela MFC, poderá perceber que é possível desenvolver um código mais específico para o seu cenário, o que pode melhorar o desempenho. Como CArray é um modelo, você pode fornecer especializações CArray para tipos específicos, por exemplo.

  • CList CList é uma lista vinculada duas vezes, por isso, a inserção do elemento é rápida no início, no fim e na posição conhecida (POSITION) na lista. A verificação de elementos por valor ou índice requer uma pesquisa sequencial, mas esse tipo de pesquisa pode ser lento se a lista for longa. Se seu código não precisar de uma lista vinculada duas vezes, reconsidere o uso de CList. Usar uma lista vinculada uma única vez evita a sobrecarga de atualizar outro ponteiro para todas as operações, bem como a memória desse ponteiro. A memória extra não é muito grande, mas ainda apresenta uma chance de falhas de cache ou falhas de página.

  • IsKindOf Essa função pode gerar muitas chamadas e acessar a memória em diferentes áreas de dados, levando a uma má localidade de referência. Ela é útil para compilações de depuração (por exemplo, em uma chamada ASSERT), mas evite usá-la na compilação de versão.

  • PreTranslateMessage Use PreTranslateMessage quando uma determinada árvore do Windows precisar de diferentes aceleradores de teclado ou quando for necessário inserir o tratamento de mensagens na bomba de mensagens. PreTranslateMessage altera as mensagens de despacho da MFC. Se você substituir PreTranslateMessage, faça isso apenas no nível necessário. Por exemplo, não é necessário substituir CMainFrame::PreTranslateMessage se você tiver interesse somente nas mensagens encaminhadas aos filhos de uma exibição específica. Em vez disso, substitua PreTranslateMessage na classe de exibição.

    Não contorne o caminho de envio normal usando PreTranslateMessage para manipular qualquer mensagem enviada a uma janela. Use procedimentos de janela e mapas de mensagens de MFC para essa finalidade.

  • OnIdle Eventos inativos poderão ocorrer em momentos inesperados, como entre os eventos WM_KEYDOWN e WM_KEYUP. Os timers podem ser uma forma mais eficiente de acionar seu código. Não force chamar OnIdle repetidamente gerando mensagens falsas ou sempre retornando TRUE ao substituir OnIdle, pois assim seu thread nunca entrará em suspensão. Nesse caso, o timer ou um thread separado pode ser mais adequado.

Bibliotecas compartilhadas

É bom poder reutilizar códigos. No entanto, se você for usar o código de outra pessoa, deve saber exatamente o que o código faz nos casos em que o desempenho é crítico. A melhor forma de saber isso é seguir o código-fonte ou dimensioná-lo com ferramentas como o PView ou o Monitor de Desempenho.

Heaps

Use diversos heaps com discrição. Os heaps adicionais criados com HeapCreate e HeapAlloc permitem que você gerencie e descarte um conjunto relacionado de alocações. Não comprometa muita memória. Se estiver usando diversos heaps, preste atenção principalmente na quantidade de memória que é inicialmente alocada.

Em vez de usar diversos heaps, você pode usar funções auxiliares para servir de interface entre seu código e o heap padrão. As funções auxiliares facilitam as estratégias de alocação personalizadas que podem melhorar o desempenho do seu aplicativo. Por exemplo, se você frequentemente faz pequenas alocações, pode querer concentrá-las em uma parte do heap padrão. Você pode alocar um grande bloco de memória e, em seguida, usar uma função auxiliar para subalocar desse bloco. Então, você não terá vários heaps com memória não utilizada, pois a alocação está sendo feita a partir do heap padrão.

No entanto, em alguns casos, o uso do heap padrão pode diminuir a localidade de referência. Use o Process Viewer, o Spy++ ou o Monitor de Desempenho para dimensionar os efeitos de mover os objetos entre os heaps.

Dimensione seus heaps para dar conta de cada alocação no heap. Use as rotinas de heap de depuraçãode tempo de execução C para verificar e despejar o heap. Você pode ler o resultado em um programa de planilhas, como o Microsoft Excel, e usar tabelas dinâmicas para exibir os resultados. Observe o número total, o tamanho e a distribuição das alocações. Compare esses resultados com o tamanho dos conjuntos de trabalho. Observe também o agrupamento dos objetos de tamanhos relacionados.

Você também pode usar os contadores de desempenho para monitorar o uso de memória.

Fios

No caso de tarefas em segundo plano, a manipulação eficiente de eventos ociosos pode ser mais rápida do que usar threads. É mais fácil entender a localidade de referência em um programa de um único segmento.

Uma boa regra é usar um thread apenas se uma notificação do sistema operacional na qual você bloqueia estiver na origem do trabalho em segundo plano. Nesse caso, os threads são a melhor solução porque é pouco prático bloquear um thread principal em um evento.

Os threads também apresentam problemas de comunicação. Você deve gerenciar o vínculo de comunicação entre seus threads por meio de uma lista de mensagens ou da alocação e do uso de memória compartilhada. O gerenciamento do vínculo de comunicação geralmente requer sincronização para evitar condições de corrida e problemas de deadlock. Essa complexidade pode resultar em bugs e problemas de desempenho.

Para obter mais informações, consulte Processamento de loop ocioso e Multithreading.

Conjunto de trabalho pequeno

Conjuntos de trabalho menores possibilitam a melhor localidade de referência, resultam em menos falhas e páginas e geram mais ocorrências de cache. O conjunto de trabalho do processo é a métrica mais próxima que o sistema operacional oferece para medir a localidade de referência diretamente.

  • Para definir os limites superior e inferior do conjunto de trabalho, use SetProcessWorkingSetSize.

  • Para obter os limites superior e inferior do conjunto de trabalho, use GetProcessWorkingSetSize.

  • Para exibir o tamanho do conjunto de trabalho, use o Spy++.

Confira também

Otimizando seu código