OK, você está definindo o problema para onde parece que não há muito espaço para melhorias. Isso é bastante raro, na minha experiência. Tentei explicar isso em um artigo do Dr. Dobbs em novembro de 1993, partindo de um programa não trivial convencionalmente bem projetado, sem desperdício óbvio, e conduzindo-o através de uma série de otimizações até que o tempo do relógio de parede fosse reduzido de 48 segundos para 1,1 segundos, e o tamanho do código-fonte foi reduzido em um fator de 4. Minha ferramenta de diagnóstico foi essa . A sequência das mudanças foi a seguinte:
O primeiro problema encontrado foi o uso de clusters de lista (agora chamados de "iteradores" e "classes de contêineres"), responsáveis por mais da metade do tempo. Eles foram substituídos por um código bastante simples, reduzindo o tempo para 20 segundos.
Agora, o maior tomador de tempo é mais construção de lista. Como porcentagem, não era tão grande antes, mas agora é porque o problema maior foi removido. Eu encontro uma maneira de acelerar, e o tempo cai para 17 segundos.
Agora é mais difícil encontrar culpados óbvios, mas existem alguns menores sobre os quais posso fazer algo, e o tempo cai para 13 segundos.
Agora eu pareço ter atingido uma parede. As amostras estão me dizendo exatamente o que está fazendo, mas não consigo encontrar nada que possa melhorar. Depois, reflito sobre o design básico do programa, sobre sua estrutura orientada a transações e pergunto se toda a pesquisa de lista que ele está fazendo é realmente exigida pelos requisitos do problema.
Então, refiz um novo design, onde o código do programa é realmente gerado (por meio de macros do pré-processador) a partir de um conjunto menor de fontes, e no qual o programa não está constantemente descobrindo coisas que o programador sabe que são bastante previsíveis. Em outras palavras, não "interprete" a sequência de coisas a fazer, "compile".
- Essa reformulação é feita, diminuindo o código-fonte por um fator de 4 e o tempo é reduzido para 10 segundos.
Agora, porque está ficando tão rápido, é difícil fazer uma amostra, por isso dou 10 vezes mais trabalho a fazer, mas os seguintes são baseados na carga de trabalho original.
Mais diagnóstico revela que está gastando tempo no gerenciamento de filas. O alinhamento destes reduz o tempo para 7 segundos.
Agora, um grande tomador de tempo é a impressão de diagnóstico que eu estava fazendo. Lave isso - 4 segundos.
Agora, os maiores tomadores de tempo são chamadas para malloc e grátis . Reciclar objetos - 2,6 segundos.
Continuando a amostra, ainda encontro operações que não são estritamente necessárias - 1,1 segundos.
Fator de aceleração total: 43,6
Agora, não existem dois programas iguais, mas em softwares que não são de brinquedos, sempre vi uma progressão como essa. Primeiro você obtém as coisas fáceis e depois as mais difíceis, até chegar a um ponto de retornos decrescentes. Então, a percepção que você obtém pode muito bem levar a uma reformulação, iniciando uma nova rodada de acelerações, até que você atinja novamente retornos decrescentes. Agora, este é o ponto em que pode fazer sentido se perguntar se ++i
ou i++
ou for(;;)
ou while(1)
são mais rápidos: os tipos de perguntas que eu vejo tantas vezes no Stack Overflow.
PS Pode-se perguntar por que não usei um criador de perfil. A resposta é que quase todos esses "problemas" eram um site de chamada de função, que empilha exemplos de maneira precisa. Ainda hoje, os criadores de perfil estão apenas chegando à ideia de que instruções e instruções de chamada são mais importantes para localizar e mais fáceis de corrigir do que funções inteiras.
Na verdade, eu criei um criador de perfil para fazer isso, mas para uma verdadeira intimidade com o que o código está fazendo, não há substituto para acertar os dedos. Não é um problema que o número de amostras seja pequeno, porque nenhum dos problemas encontrados é tão pequeno que é facilmente esquecido.
ADICIONADO: jerryjvl solicitou alguns exemplos. Aqui está o primeiro problema. Consiste em um pequeno número de linhas de código separadas, que levam mais de metade do tempo:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Eles estavam usando o cluster de lista ILST (semelhante a uma classe de lista). Eles são implementados da maneira usual, com "ocultação de informações", significando que os usuários da classe não deveriam ter que se importar com como eles foram implementados. Quando essas linhas foram escritas (de aproximadamente 800 linhas de código), não se pensou na idéia de que elas poderiam ser um "gargalo" (eu odeio essa palavra). Eles são simplesmente a maneira recomendada de fazer as coisas. É fácil dizer, em retrospectiva, que eles deveriam ter sido evitados, mas, na minha experiência, todos os problemas de desempenho são assim. Em geral, é bom tentar evitar a criação de problemas de desempenho. É ainda melhor encontrar e consertar os que são criados, mesmo que "devam ter sido evitados" (em retrospectiva).
Aqui está o segundo problema, em duas linhas separadas:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Essas são listas de construção anexando itens a seus fins. (A correção foi coletar os itens em matrizes e criar as listas de uma só vez.) O interessante é que essas instruções custam apenas (ou seja, estavam na pilha de chamadas) 3/48 do tempo original, portanto, não estavam dentro fato um grande problema no começo . No entanto, depois de remover o primeiro problema, eles custaram 3/20 do tempo e agora eram um "peixe maior". Em geral, é assim que acontece.
Devo acrescentar que este projeto foi destilado de um projeto real no qual ajudei. Nesse projeto, os problemas de desempenho eram muito mais dramáticos (assim como as acelerações), como chamar uma rotina de acesso ao banco de dados dentro de um loop interno para verificar se uma tarefa foi concluída.
REFERÊNCIA ADICIONADA: O código fonte, original e reprojetado, pode ser encontrado em www.ddj.com , para 1993, no arquivo 9311.zip, nos arquivos slug.asc e slug.zip.
EDIT 26/11/2011: Agora existe um projeto SourceForge contendo código-fonte no Visual C ++ e uma descrição detalhada de como foi ajustada. Ele passa apenas pela primeira metade do cenário descrito acima e não segue exatamente a mesma sequência, mas ainda recebe uma aceleração de ordem de magnitude 2-3.