Tudo é rápido para as pequenas empresas

Digamos que o senhor esteja prestes a implementar um aplicativo. Esse aplicativo foi amplamente testado pela sua equipe de desenvolvimento, que foi infectada pela febre dos testes unitários. Ele também foi examinado pelo seu grupo de controle de qualidade, que passou meses vasculhando cada fenda do aplicativo. O senhor até teve um período de teste beta com usuários reaisque, obedientemente, registraram bugs e testaram o aplicativo antes de finalmente aprová-lo.

Seu aplicativo é útil e popular. Seus usuários o adoram. Seus usuários adoram o senhor. Mas, na semana seguinte, algo curioso acontece. À medida que as pessoas usam o aplicativo, ele fica cada vez mais lento. Logo, as reclamações começam a chegar. Em poucas semanas, o aplicativo está quase inutilizável devido a todos os atrasos insuportáveis a que submete os usuários – e seus usuários se voltam contra o senhor.

Levante a mão se isso já aconteceu em algum projeto em que o senhor trabalhou. Se eu ganhasse um dólar por cada vez que vi isso pessoalmente, teria o suficiente para um bom almoço. Os desenvolvedores testam com pequenos conjuntos de dados de brinquedo, presumem que tudo está bem e depois descobrem da maneira mais difícil que tudo é rápido para n pequeno.

Lembro-me de uma rotina de classificação em Javascript no lado do cliente que implementamos em um aplicativo da Web de intranet avançada por volta de 2002. Ela funcionou muito bem em nossos pequenos conjuntos de dados de teste, mas quando a implementamos na produção, ficamos surpresos ao descobrir que a classificação de uma mísera centena de itens poderia levar mais de 5 segundos na máquina desktop de um usuário. O JavaScript não é conhecido por sua velocidade, mas que diabos?

Bem, adivinhe qual algoritmo de classificação que usamos?

animação de algoritmos de classificação

O InsertSort é n2 (pior caso), o ShellSort é n3/2e o QuickSort é n log n. Mas nós poderia ter feito pior – poderíamos ter escolhido o Bubble Sort, que é n2 mesmo no melhor caso.

Amigos, não façam isso. Teste seus aplicativos com grandes conjuntos de dados, pelo menos grandes o suficiente para cobrir suas projeções de uso mais otimistas para o próximo ano. Caso contrário, o senhor poderá se arrepender. E seus usuários certamente irão se arrepender.

A notação Big O é uma daquelas coisas que é mais fácil de ver do que de explicar. Mas é um bloco de construção fundamental da ciência da computação.

Notação Big O: Uma medida teórica da execução de um algoritmoO algoritmo é uma medida de tempo, geralmente o tempo ou a memória necessária, dado o tamanho do problema n, que geralmente é o número de itens. Informalmente, dizer que alguma equação f(n) = O(g(n)) significa que ela é menor do que algum múltiplo constante de g(n). A notação é a seguinte: “f de n é grande oh de g de n”.

Os desenvolvedores contam com estruturas de dados e bancos de dados que têm um desempenho favorável da notação big O incorporado, sem pensar muito sobre isso. Mas se o senhor se desviar desses caminhos bem usados, o senhor pode se deparar com um mundo de problemas de desempenho – e muito mais cedo do que poderia imaginar. A importância da notação big O é melhor ilustrada neste gráfico do Pérolas de programação:

tempo de execução no TRS-80 vs. Dec Alpha

O TRS-80 algoritmo é 48ne o DEC Alpha O algoritmo é n3.

Quando n é 10, eles estão a um segundo um do outro. Mas quando n aumenta para 100.000, o DEC Alpha moderno leva um mês para fazer o que um TRS-80 pré-histórico pode fazer em algumas horas. Ter um grande gargalo de notação O em seu aplicativo é uma passagem só de ida na máquina de retrocesso de desempenho para 1997 – ou pior. Muito pior.

Existem nomes amigáveis para as notações comuns de big O; dizer “n ao quadrado” é equivalente a dizer “quadrático”:

Tom Niemann tem gráficos úteis que comparam as taxas de crescimento de algoritmos comuns, que eu adaptei aqui:

n lg n n7/6 n lg n n2 7/6n n!
1 0 1 0 1 1 1
16 4 25 64 256 12 20,9 trilhões
256 8 645 2,048 65,536 137 quatrilhões
4,096 12 16,384 49,152 16,777,216
65,536 16 416,128 1,048,565 4,294,967,296
1,048,476 20 10,567,808 20,969,520 1,09 trilhão
16,775,616 24 268,405,589 402,614,784 281,4 trilhões

Aqui estão exemplos de tempos de execução, supondo que uma unidade de execução seja igual a um milissegundo de tempo de CPU. Isso provavelmente é um exagero nas CPUs atuais, mas o senhor entendeu a ideia:

n lg n n7/6 n lg n n2 7/6n n!
1 <1 seg <1 seg <1 seg <1 seg <1 seg <1 seg
16 <1 seg <1 seg <1 seg <1 seg <1 seg 663 anos
256 <1 seg <1 seg 2 segundos 65 segundos 4,3 milhões de anos
4,096 <1 seg 16 seg 49 seg 4,6 horas
65,536 <1 seg 7 min 17 min 50 dias
1,048,476 <1 seg 3 horas 6 horas 35 anos
16,775,616 <1 seg 3 dias 4,6 dias 8.923 anos

Observe a rapidez com que temos problemas à medida que o número de itens (n) aumenta. A menos que o senhor tenha escolhido estruturas de dados que ofereçam desempenho ideal quase logarítmico em todas as áreas, quando chegar a 4.096 itens, estará falando de grave tempo de CPU. Além disso, usei o como abreviação de “forever” (para sempre). É assim que as coisas podem ficar ruins.

Tudo é rápido para as pequenas empresas. Não caia nessa armadilha. É um erro bastante fácil de cometer. Os aplicativos modernos são incrivelmente complexos, com dezenas de dependências. Se o senhor deixar de indexar um pequeno campo no banco de dados, de repente estará de volta à terra do TRS-80. A única maneira de saber realmente se o senhor inseriu acidentalmente um gargalo algorítmico big O em algum lugar do seu aplicativo é testá-lo com um volume razoavelmente grande de dados.