A triste tragédia do teatro de micro-otimização

Vou ser direto e dizer isso: Eu adoro cordas. No que me diz respeito, não há um problema que eu não possa resolver com uma string e talvez uma ou duas expressões regulares. Mas talvez esse seja apenas o meu falta de habilidades matemáticas falando.

No entanto, falando sério, o tipo de programação que fazemos no Stack Overflow está intimamente ligado a strings. Estamos constantemente construindo-as, mesclando-as, processando-as ou despejando-as em um fluxo HTTP. Às vezes, até faço massagens relaxantes nelas. Agora, se o senhor já trabalhou com strings, sabe que esse é um código que deve ser evitado desesperadamente:

static string Shlemiel()
{
string result = "";
for (int i = 0; i < 314159; i++)
{
result += getStringData(i);
}
return result;
}

Na maioria das linguagens com coleta de lixo, as cadeias de caracteres são imutáveis: quando o senhor adiciona duas cadeias de caracteres, o conteúdo de ambas é copiado. À medida que o usuário continua adicionando result nesse loop, mais e mais memória é alocada a cada vez. Isso leva diretamente ao terrível quadrádico n2 desempenhoou, como Joel gosta de chamá-lo, Desempenho de Shlemiel, o pintor.

Quem é Shlemiel? É o senhor que está nessa piada:

Shlemiel consegue um emprego como pintor de rua, pintando as linhas pontilhadas no meio da rua. No primeiro dia, ele leva uma lata de tinta para a rua e termina 300 metros de estrada. “Isso é muito bom!”, diz seu chefe, “o senhor é um trabalhador rápido!” e lhe paga um copeque.

No dia seguinte, Shlemiel só conseguiu trabalhar 150 jardas. “Bem, isso não é nem de longe tão bom quanto ontem, mas o senhor ainda é um trabalhador rápido. 150 jardas é respeitável”, e lhe paga um copeque.

No dia seguinte, Shlemiel pinta 30 metros da estrada. “Apenas 30!”, grita seu chefe. “Isso é inaceitável! No primeiro dia, o senhor fez dez vezes mais trabalho! O que está acontecendo?”

“Não consigo evitar”, diz Shlemiel. “Todos os dias fico cada vez mais longe da lata de tinta!”

Essa é uma pergunta de softball. Todos os senhores sabiam disso. Todos os programador decente sabe que a concatenação de strings, embora seja boa em pequenas doses, é um veneno mortal em loops.

Mas e se o senhor não estiver fazendo nada além de pequenos trechos de concatenação de strings, dezenas a centenas de vezes, como na maioria dos aplicativos da Web? Então, o senhor pode desenvolver uma dúvida incômoda, como eu tive, de que muitos pequenos Shlemiels poderiam ser tão ruins quanto um gigante Shlemiel.

Digamos que o senhor queira criar esse fragmento HTML:

<div class="user-action-time">stuff</div>
<div class="user-gravatar32">stuff</div>
<div class="user-details">stuff<br/>stuff</div>

que pode aparecer em uma determinada página do Stack Overflow de uma a sessenta vezes. E estamos servindo centenas de milhares dessas páginas por dia.

Agora não está tão claro, não é mesmo?

Então, qual desses métodos de formação da string acima o senhor acha que é o mais rápido em cem mil iterações?

1: Concatenação simples

string s =
@"<div class=""user-action-time"">" + st() + st() + @"</div>
<div class=""user-gravatar32"">" + st() + @"</div>
<div class=""user-details"">" + st() + "<br/>" + st() + "</div>";
return s;

2: String.Format

string s =
@"<div class=""user-action-time"">{0}{1}</div>
<div class=""user-gravatar32"">{2}</div>
<div class=""user-details"">{3}<br/>{4}</div>";
return String.Format(s, st(), st(), st(), st(), st());

3: string.Concat

string s =
string.Concat(@"<div class=""user-action-time"">", st(), st(),
@"</div><div class=""user-gravatar32"">", st(),
@"</div><div class=""user-details"">", st(), "<br/>",
st(), "</div>");
return s;

4: String.Replace

string s =
@"<div class=""user-action-time"">{s1}{s2}</div>
<div class=""user-gravatar32"">{s3}</div>
<div class=""user-details"">{s4}<br/>{s5}</div>";
s = s.Replace("{s1}", st()).Replace("{s2}", st()).
Replace("{s3}", st()).Replace("{s4}", st()).
Replace("{s5}", st());
return s;

5: StringBuilder

var sb = new StringBuilder(256);
sb.Append(@"<div class=""user-action-time"">");
sb.Append(st());
sb.Append(st());
sb.Append(@"</div><div class=""user-gravatar32"">");
sb.Append(st());
sb.Append(@"</div><div class=""user-details"">");
sb.Append(st());
sb.Append("<br/>");
sb.Append(st());
sb.Append("</div>");
return sb.ToString();

Tire seu dedo do gatilho da tecla de compilação e pense sobre isso por um minuto. Qual desses métodos será mais rápido?

O senhor tem uma resposta? Ótimo!

E… rufem os tambores, por favor… a resposta correta:

Já sabemos que nenhuma dessas operações será executada em um loop, portanto, podemos descartar as características de desempenho brutalmente ruim da concatenação ingênua de strings. Tudo o que resta é a micro-otimização, e no momento em que o senhor começa a se preocupar com pequenas otimizações, o senhor já seguiu o caminho errado.

Oh, o senhor não acredita em mim? Infelizmente, eu mesmo não acreditei, e é por isso que fui atraído para isso em primeiro lugar. Aqui estão meus resultados – para 100.000 iterações, em um Core 2 Duo dual core de 3,5 GHz.

1: Concatenação simples 606 ms
2: String.Format 665 ms
3: string.Concat 587 ms
4: String.Replace 979 ms
5: StringBuilder 588 ms

Mesmo que passássemos do pior com a melhor técnica, teríamos economizado míseros 391 milissegundos em cem mil iterações. Não é o tipo de coisa pela qual eu daria uma festa de vitória. Acho que descobri que usar o .Replace é melhor evitar, mas mesmo isso tem alguns benefícios de legibilidade que podem compensar o custo minúsculo.

Agora, o senhor pode muito bem perguntar qual dessas técnicas tem a menor uso de memória, como fez Rico Mariani. Não tive a chance de comparar com o CLRProfiler para ver se havia um vencedor claro nesse aspecto. É um ponto válido, mas duvido que os resultados mudem muito. Na minha experiência, as técnicas que abusam da memória também tendem a consumir muito tempo do relógio. As alocações de memória são rápidas nos PCs modernos, mas estão longe de ser gratuitas.

As opiniões variam sobre apenas quantas cadeias de caracteres o senhor precisa concatenar antes que o senhor comece a se preocupar com o desempenho. O consenso geral é cerca de 10. Mas o senhor também lerá coisas malucas, como esta:

Não use += concatenando nunca. Muitas mudanças estão ocorrendo nos bastidores, que não são óbvias no meu código. Aconselho o senhor a usar String.Concat() explicitamente com qualquer sobrecarga (2 strings, 3 strings, matriz de strings). Isso mostrará claramente o que o seu código faz sem surpresas e, ao mesmo tempo, permitirá que o senhor controle a eficiência.

Nunca? Nunca? Nunca, jamais, nunca? Nem mesmo uma vez? Nem mesmo se isso não importa? Sempre que o senhor vir “nunca faça X”, os alarmes devem soar. Como esperamos que estejam agora.

Sim, o senhor deve evitar os erros óbvios de concatenação de strings para iniciantes, o que todo programador aprende no primeiro ano de trabalho. Mas depois disso, o senhor deve se preocupar mais com a manutenção e a legibilidade do seu código do que com o desempenho dele. E esse talvez seja o aspecto mais trágico de se deixar levar pelo teatro da micro-otimização. isso o distrai do seu verdadeiro objetivo: escrever um código melhor.