Otimização fina de código

Há um tempo atrás num post polêmico do Miguel (ando falando demais no passado né? mas tem coisas que não dá pra deixar passar…) o Raphael escreveu um comentário que me incomodou:

Performance? Dane-se! Python é mais lento que Java? Sem dúvida. Mas performance pura não importa hoje em dia. Hardware hoje já é tão barato que pouco importa se eu tenho que ter 1, 2 ou 3 máquinas para rodar o sistema. O que não pode é ter uma plataforma tão lenta que me obrigue a comprar 20 ou 30, que seria o caso do Ruby. Questão de ordem de grandeza.
É claro que eu tenho plena conciência que o Raphael não acredita 100% na afirmação acima e que ele estava envolvido numa discussão (quase) sem sentido com o Bruno… :P Prova disso é que mais pra frente, em outro comentário ele escreveu:
Eu quero uma stack de protocolo de comunicação SIP que atenda 100 mil usuários simultâneos
Que vai exatamente contra o que ele tinha dito antes! Afinal de contas, neste exemplo desempenho é primordial ou nada funciona. Talvez por isso eu estivesse achando a discussão (quase) sem sentido… Mas… vamos ao que (me) interessa!

O fato é que desempenho importa sim em muitos casos. Em algumas áreas ele é, eu diria, primordial. Mais essencial até que uma boa interface homem computador. Duvida? Pense então em criptografia.

Quase todo mundo hoje em dia sabe o que é criptografia e quase ninguém duvida que ela é essencial para nossa (falsa?) segurança na Internet. O fato que pouca gente sabe é que ainda hoje algoritmos criptográficos são exageradamente pesados. Aliás, para o desespero de alguns, é exatamente aí que está o segredo: não existe, ainda hoje, nenhuma prova formal de que algoritmos criptográficos sejam realmente seguros. O que todo mundo sabe é que é muito difícil quebrá-los com o que se conhece de teoria matemática hoje em dia. E é nessa dificuldade que é baseada boa parte da segurança das comunicações mundiais. Parece frágil isso, não? Definitivamente! E olha que eu nem comecei a falar nas técnicas não-matemáticas para se quebrar um algoritmo criptográfico… Mas isso não é conversa para este post.

Neste post, antes que eu perca o fio da meada de novo, quero falar de desempenho! Como algoritmos criptográficos ainda hoje são muito pesados, é muito comum você achar artigos e artigos na Internet de autores se vangloriando por terem achado um novo método revolucionário de implementar um algoritmo criptográfico que traz um ganho de desempenho de incríveis 5%! Ou ainda pesquisadores gastando meses fazendo testes para descobrir a combinação de flags de otimização a serem usadas no gcc para fornecer um ganho de 3%!

E é ai que eu queria chegar. Estudando um pouco sobre criptografia este semestre eu aprendi com o professor Julio López da Unicamp um método de otimização de código capaz de fazer com que um código rode consideravelmente mais rápido. Pense no seguinte trecho de código, bastante comum em algoritmos criptográficos: if (a & 0x80) r ^= b; Basicamente o que se deseja fazer é testar um bit da variável a. Se este bit for 1 faz-se uma operação de soma em aritmética módulo 2 (um XOR) da variável b sobre um acumulador da resposta r. Este tipo de código, apesar de simples, costuma ser executado de forma muito ineficiente em processadores de propósito geral modernos. Isso porque este processadores são muito rápidos mas não são adivinhos. Para simplificar bastante o assunto, podemos dizer que antes de eles terem idéia do resultado do if eles já estão tentando executar a instrução em seguida (a soma). No entanto, eles podem descobrir alguns ciclos de relógio depois que a soma não deveria ser executada pois o if deu falso. Neste caso eles tem que voltar atrás e desfazer o deslocamento.

Os processadores modernos até tentam adivinhar qual caminho é o certo, mas nem sempre isso dá certo. Descobrir que o que ele estava fazendo estava errado e voltar atrás é algo que penaliza muito o processador. Perde-se às vezes dezenas de ciclos de relógio com isso… Tempo suficiente para executar talvez de 6 a 10 instruções, dependendo do processador. Ou seja, se conseguíssemos eliminar este if do código poderíamos ter um ganho. O código para eliminar o if acima é o seguinte: r ^= b & (-((a & 0x80) >> 7)); Alguém vai dizer: “Ora bolas, mas agora ao invés de um if, que é em geral uma comparação e um salto condicional, e um XOR eu estou fazendo um XOR, dois AND, um deslocamento e uma inversão de sinal!” Por incrível que pareça isso, muitas vezes, é mais rapido, pois elimina qualquer possibilidade do processador ter que voltar atrás e jogar fora processamento, perdendo dezenas de ciclos de relógio de execução.

É este tipo de truque de programação que muitos pesquisados da área de criptografia passam meses tentando descobrir. Este truque, aliás, é bem conhecido. Ele é muitos outros podem ser vistos no livro Hacker’s Delight. Nunca olhei o livro profundamente mas ele já me foi muito bem recomendado.

E você tem algum truque bacana destes pra nos ensinar?

  • Raphael
    ôôô Léo... Péra um pouco. Já disse o poeta: uma coisa é uma coisa, outra coisa é outra coisa. O lance de otimizar código explorando características da arquitetura da máquina é legal e tem suas aplicações (Existe um esquema para fazer divisão inteira por 10 que é usado há muito tempo) e sempre vai ter gente forçando esse tipo de limite, quer por questões acadêmicas, quer por questões de competição de mercado. O problema é a relação custo x performance. Um processador "top", que acabou de sair do forno, custa 10 vezes mais que um processador "moderno". Mas a diferença de performance não chega a ser de 10 vezes, não é mesmo? Essa diferença de preço é meio artificial e pode ser atribuida à forma que as empresas fazem o "product cycle", mas é outra história. O ponto: performance para o aplicativo médio já não é um critério relevante. É por isso que as empresas que produzem processadores já estão tirando o foco do quesito "velocidade de processamento" e focando no ponto "performance/watt". Diria até o seguinte: se não fosse pelos mercados de aplicações científicas e de videogames, as fabricantes de processadores estariam numa situação parecida com a da MS, que tem um produto na mão com recursos que não são desejados pela maioria do mercado consumidor. De qualquer forma, meu ponto na minha discussão sem sentido é que mesmo as máquinas "menos modernas" já dão conta do recado muito bem dos requisitos de capacidade de processamento. Até mesmo as "centrais SIP com 100 mil acessos simultâneos" não precisam de processadores poderosos. Basta ter meia dúzia de PowerPC, algumas placas em redundância e uma saída de fibra óptica. O hardware de uma central dessas até seria barato. Caro é a tecnologia que vai em cima, e o serviço para gerenciamento disso. :P
  • Leandro Lameiro
    Ol&aacute; Tenho um truque legal sim. Incentivado por este post, fiz um post no meu blog ( <a href="http://lameiro.wordpress.com" target="_blank">http://lameiro.wordpress.com ) sobre o xor swap, um truque para trocar 2 vari&aacute;veis sem usar uma vari&aacute;vel tempor&aacute;ria. Abra&ccedil;os e parab&eacute;ns pelo blog. Leandro Lameiro
  • capitanio
    Estava com saudades dos seus posts... Solta o sapato no Lellis :) Grande abra&ccedil;o, Capitanio
  • Raphael
    ôôô Léo... Péra um pouco. Já disse o poeta: uma coisa é uma coisa, outra coisa é outra coisa.

    O lance de otimizar código explorando características da arquitetura da máquina é legal e tem suas aplicações (Existe um esquema para fazer divisão inteira por 10 que é usado há muito tempo) e sempre vai ter gente forçando esse tipo de limite, quer por questões acadêmicas, quer por questões de competição de mercado.

    O problema é a relação custo x performance. Um processador "top", que acabou de sair do forno, custa 10 vezes mais que um processador "moderno". Mas a diferença de performance não chega a ser de 10 vezes, não é mesmo?

    Essa diferença de preço é meio artificial e pode ser atribuida à forma que as empresas fazem o "product cycle", mas é outra história.

    O ponto: performance para o aplicativo médio já não é um critério relevante. É por isso que as empresas que produzem processadores já estão tirando o foco do quesito "velocidade de processamento" e focando no ponto "performance/watt". Diria até o seguinte: se não fosse pelos mercados de aplicações científicas e de videogames, as fabricantes de processadores estariam numa situação parecida com a da MS, que tem um produto na mão com recursos que não são desejados pela maioria do mercado consumidor.

    De qualquer forma, meu ponto na minha discussão sem sentido é que mesmo as máquinas "menos modernas" já dão conta do recado muito bem dos requisitos de capacidade de processamento. Até mesmo as "centrais SIP com 100 mil acessos simultâneos" não precisam de processadores poderosos. Basta ter meia dúzia de PowerPC, algumas placas em redundância e uma saída de fibra óptica. O hardware de uma central dessas até seria barato. Caro é a tecnologia que vai em cima, e o serviço para gerenciamento disso. :P
  • Olá

    Tenho um truque legal sim. Incentivado por este post, fiz um post no meu blog ( http://lameiro.wordpress.com ) sobre o xor swap, um truque para trocar 2 variáveis sem usar uma variável temporária.

    Abraços e parabéns pelo blog.
    Leandro Lameiro
  • Estava com saudades dos seus posts...
    Solta o sapato no Lellis :)

    Grande abraço,

    Capitanio
  • Leonardo Garcia
    Eita Lullis! hehehehe.... Tava esperando sua resposta &agrave; minha provoca&ccedil;&atilde;o. :P E tenho que adimitir: uma coisa &eacute; uma coisa, outra coisa &eacute; outra coisa. S&oacute; peguei duas frases suas da mesma discuss&atilde;o mas em contexto diferentes e olha que que eu fiz :) De qualquer forma, acho que foi um bom gancho para meu argumento. :) Um abra&ccedil;o, Leonardo Garcia
  • Eita Lullis! hehehehe....

    Tava esperando sua resposta à minha provocação. :P

    E tenho que adimitir: uma coisa é uma coisa, outra coisa é outra coisa. Só peguei duas frases suas da mesma discussão mas em contexto diferentes e olha que que eu fiz :)

    De qualquer forma, acho que foi um bom gancho para meu argumento. :)

    Um abraço,

    Leonardo Garcia
  • Bruno
    Domingo, 3AM, o sistema entra em produ&ccedil;&atilde;o daqui algumas horas. Como diferenciar os statements abaixo? r ^= b &amp; (-((a &amp; 0x80) &gt;&gt; 0x7)); r ^= b &amp; ((-(a &amp; 0x80) &gt;&gt; 07)); r |= r &amp; ((-(b &amp; 0x08) &gt;&gt; 7)); r |= b &amp; ((-(a &amp; 0x80) &lt;&gt; 0x7)); r |= r &amp; ((-(a &amp; 0x80) &gt;&gt; 7)); r ^= b &amp; (-((a &amp; 0x08) &lt;&gt; 7)); r ^= r &amp; (-((b &amp; 0x08) &lt;&lt; 0x7)); Para sacrificar a legibilidade de maneira t&atilde;o atroz s&oacute; com provas irrefut&aacute;veis da necessidade do c&oacute;digo acima ;-)
  • Leonardo Garcia
    Bruno, Concordo com voc&ecirc;! E &eacute; por isso que eu dei o meu exemplo em uma classe bem espec&iacute;fica de problemas: algoritmos criptogr&aacute;ficos. Nesta classe de problemas efici&ecirc;ncia &eacute; muito mais importante que legibilidade de c&oacute;digo. Quer um exemplo: OpenSSL, a biblioteca de criptografia mais usada no mundo. Se voc&ecirc; ler, por exemplo, a especifica&ccedil;&atilde;o do AES, o algoritmo &eacute; relativamente simples de entender. Pegue a implementa&ccedil;&atilde;o do AES no OpenSSL por&eacute;m e veja que existe uma diferen&ccedil;a relativamente grande entre o que est&aacute; na especifica&ccedil;&atilde;o e o que foi implementado. Como voc&ecirc; bem disse, &eacute; o tipo de coisa que ningu&eacute;m vai querer num sistema que ir&aacute; entrar em produ&ccedil;&atilde;o em algumas horas ou que precise de manuten&ccedil;&atilde;o constante. Mas em aplica&ccedil;&otilde;es que rodam em ambientes n&atilde;o muito favor&aacute;veis como os algoritmos criptogr&aacute;ficos ou os microcontroladores citados pelo Leandro Lameiro acima, estes truques ileg&iacute;veis muitas vezes s&atilde;o as &uacute;nicas sa&iacute;das. Um abra&ccedil;o, Leonardo Garcia
  • Domingo, 3AM, o sistema entra em produção daqui algumas horas. Como diferenciar os statements abaixo?

    r ^= b & (-((a & 0x80) >> 0x7));
    r ^= b & ((-(a & 0x80) >> 07));
    r |= r & ((-(b & 0x08) >> 7));
    r |= b & ((-(a & 0x80) <> 0x7));
    r |= r & ((-(a & 0x80) >> 7));
    r ^= b & (-((a & 0x08) <> 7));
    r ^= r & (-((b & 0x08) << 0x7));

    Para sacrificar a legibilidade de maneira tão atroz só com provas irrefutáveis da necessidade do código acima ;-)
  • Bruno,

    Concordo com você! E é por isso que eu dei o meu exemplo em uma classe bem específica de problemas: algoritmos criptográficos.

    Nesta classe de problemas eficiência é muito mais importante que legibilidade de código. Quer um exemplo: OpenSSL, a biblioteca de criptografia mais usada no mundo. Se você ler, por exemplo, a especificação do AES, o algoritmo é relativamente simples de entender. Pegue a implementação do AES no OpenSSL porém e veja que existe uma diferença relativamente grande entre o que está na especificação e o que foi implementado.

    Como você bem disse, é o tipo de coisa que ninguém vai querer num sistema que irá entrar em produção em algumas horas ou que precise de manutenção constante. Mas em aplicações que rodam em ambientes não muito favoráveis como os algoritmos criptográficos ou os microcontroladores citados pelo Leandro Lameiro acima, estes truques ilegíveis muitas vezes são as únicas saídas.

    Um abraço,

    Leonardo Garcia
  • Bruno
    L. Garcia, Acho que voc&ecirc; j&aacute; tinha deixado isto bem claro no post, foi mais para enfatizar um pouco mais ;) [ ]&#039;s!!
  • L. Garcia,

    Acho que você já tinha deixado isto bem claro no post, foi mais para enfatizar um pouco mais ;)

    [ ]'s!!
  • Phil Souza
    Bom post. Gerou uma discuss&atilde;o interessante, mas minha vida como programador ainda &eacute; muito nova (n&atilde;o, eu entendi tudo aqui), mas prefiro n&atilde;o comentar e depois falar besteira... Como dizem &quot;&eacute; melhor ficar calado e acharem que voc&ecirc; &eacute; um idiota do que voc&ecirc; falar e terem certeza...&quot; Bom, j&aacute; falei :D
  • Leonardo Garcia
    Phil, N&atilde;o sei quem disse a frase &quot;&eacute; melhor ficar calado e acharem que voc&ecirc; &eacute; um idiota do que voc&ecirc; falar e terem certeza...&quot; mas j&aacute; a escutei v&aacute;rias vezes e conhe&ccedil;o v&aacute;rias pessoas que ficam intimidadas por esta id&eacute;ia. Mas, na minha opini&atilde;o, a gente tem sempre que se espressar. &Eacute; claro que devemos pensar antes de falar porque sen&atilde;o pode sair s&oacute; besteira mesmo e ai a coisa fica muito feia. Mas se n&atilde;o nos espressamos e falarmos o que pensamos n&atilde;o h&aacute; possibilidade de nos corrigirmos. Ali&aacute;s, tamb&eacute;m n&atilde;o existe possibilidade de influenciarmos os outros e de constru&iacute;rmos id&eacute;ias novas a partir dos diferentes pontos de vista que s&oacute; s&atilde;o poss&iacute;veis devido &agrave;s diferen&ccedil;as de vis&atilde;o de mundo de cada pessoa. N&atilde;o quero for&ccedil;ar-lhe a falar. Acho que temos que fazer aquilo que nos &eacute; confort&aacute;vel. Mas da minha parte mesmo voc&ecirc; provavelmente sempre ouvir&aacute; eu falar ( (quase sempre) depois de pensar no que eu vou falar). Ali&aacute;s, neste blog mesmo, o primeiro artigo que eu escrevi (que pode ser lido em <a href="http://log4dev.com/2006/08/17/redes-e-o-futuro-da..." target="_blank">http://log4dev.com/2006/08/17/redes-e-o-futuro-da... foi refutado. E n&atilde;o vi problema nenhum sobre isso. N&atilde;o parei de escrever. Continuei, com a certeza de que s&oacute; atrav&eacute;s de conversas e discuss&otilde;es que o mundo evolui. Um abra&ccedil;o, Leonardo Garcia
  • Bom post. Gerou uma discussão interessante, mas minha vida como programador ainda é muito nova (não, eu entendi tudo aqui), mas prefiro não comentar e depois falar besteira...

    Como dizem "é melhor ficar calado e acharem que você é um idiota do que você falar e terem certeza..."

    Bom, já falei :D
  • Phil,

    Não sei quem disse a frase "é melhor ficar calado e acharem que você é um idiota do que você falar e terem certeza..." mas já a escutei várias vezes e conheço várias pessoas que ficam intimidadas por esta idéia.

    Mas, na minha opinião, a gente tem sempre que se espressar. É claro que devemos pensar antes de falar porque senão pode sair só besteira mesmo e ai a coisa fica muito feia. Mas se não nos espressamos e falarmos o que pensamos não há possibilidade de nos corrigirmos. Aliás, também não existe possibilidade de influenciarmos os outros e de construírmos idéias novas a partir dos diferentes pontos de vista que só são possíveis devido às diferenças de visão de mundo de cada pessoa.

    Não quero forçar-lhe a falar. Acho que temos que fazer aquilo que nos é confortável. Mas da minha parte mesmo você provavelmente sempre ouvirá eu falar ( (quase sempre) depois de pensar no que eu vou falar). Aliás, neste blog mesmo, o primeiro artigo que eu escrevi (que pode ser lido em http://log4dev.com/2006/08/17/redes-e-o-futuro-...) foi refutado. E não vi problema nenhum sobre isso. Não parei de escrever. Continuei, com a certeza de que só através de conversas e discussões que o mundo evolui.

    Um abraço,

    Leonardo Garcia
  • Phil Souza
    Sem problemas Leonardo, foi mais uma brincadeira que em si algo s&eacute;rio, mas os assuntos nos coment&aacute;rios ficou mais que resolvido me parece. Nada acrescentar :D Abra&ccedil;os!
  • Sem problemas Leonardo, foi mais uma brincadeira que em si algo sério, mas os assuntos nos comentários ficou mais que resolvido me parece. Nada acrescentar :D
    Abraços!
  • Phil, a vida é muito curta, solta o verbo!!! :)
  • Leonardo Garcia
    &Oacute;timo exemplo, Bruno! :)
  • Ótimo exemplo, Bruno! :)
  • Ricardo
    Hi, hoje em dia o primeiro c&oacute;digo pode ser bem mais r&aacute;pido que o segundo. Se voc&ecirc; tiver uma cpu moderna e um compilador moderno, o primeiro if pode ser compilado para um opcode cmov, que n&atilde;o tem branch e nem segura a pipeline. O segundo c&oacute;digo tem uma trilha muito longa de depend&ecirc;ncia de dados, ent&atilde;o o c&oacute;digo com cmov acaba sendo mais amig&aacute;vel pra cpus com out-of-order execution.
  • Hi, hoje em dia o primeiro código pode ser bem mais rápido que o segundo. Se você tiver uma cpu moderna e um compilador moderno, o primeiro if pode ser compilado para um opcode cmov, que não tem branch e nem segura a pipeline. O segundo código tem uma trilha muito longa de dependência de dados, então o código com cmov acaba sendo mais amigável pra cpus com out-of-order execution.
blog comments powered by Disqus

Switch to our mobile site