Donald Knuth dijo una vez las palabras que luego se hicieron famosas: “El verdadero problema es que los programadores, no donde necesitan, y no cuando lo necesitan, pasan demasiado tiempo cuidando la eficiencia. La optimización prematura es la raíz de todos los males (o al menos la mayoría de ellos) en la programación ”.

El autor del material, cuya traducción publicamos hoy, quiere hablar sobre cómo una vez cayó en la trampa de la optimización prematura, y cómo entendió por su propia amarga experiencia que la optimización prematura es la raíz de todos los males.
Juego GeoArena en línea
Hace unos años trabajé en el juego web GeoArena Online (luego lo
vendí , los nuevos propietarios lo publicaron en
geoarena.io ). Fue un juego multijugador al estilo de "el último sobreviviente". Allí, el jugador controlaba la nave, luchando uno contra uno contra otro jugador.
Juego GeoArena en líneaJuego GeoArena en líneaUn juego dinámico, cuyo mundo está lleno de partículas y efectos, requiere recursos informáticos serios. Como resultado, el juego en algunas computadoras viejas "se ralentizó" en momentos particularmente tensos. Yo, un hombre que no es indiferente a los problemas de productividad, tomé la solución a este problema con interés. "¿Cómo puedo acelerar la parte de JavaScript del lado del cliente de GeoArena?", Me pregunté.
Biblioteca Fast.js
Después de buscar un poco en Internet, descubrí la biblioteca
fast.js. Era una "colección de micro optimizaciones destinadas a simplificar el desarrollo de programas JavaScript muy rápidos". Esta biblioteca fue acelerada por la disponibilidad de implementaciones más rápidas de los métodos estándar integrados como
Array.prototype.forEach () .
Esto me pareció extremadamente interesante. GeoArena usó muchas matrices, realizó muchas operaciones con matrices, por lo que usar fast.js podría ayudarme a acelerar el juego. Los siguientes resultados del estudio de rendimiento
forEach()
se incluyeron en
README para fast.js.
Native .forEach() vs fast.forEach() (10 items) ✓ Array::forEach() x 8,557,082 ops/sec ±0.37% (97 runs sampled) ✓ fast.forEach() x 8,799,272 ops/sec ±0.41% (97 runs sampled) Result: fast.js is 2.83% faster than Array::forEach().
¿Cómo puede un método implementado en alguna biblioteca externa ser más rápido que su versión estándar? El caso es que hubo un truco (ellos, estos trucos, se encuentran en todas partes). La biblioteca solo era adecuada para trabajar con matrices que no eran escasas.
Aquí hay un par de ejemplos simples de tales matrices:
Para entender por qué la biblioteca no puede funcionar normalmente con matrices dispersas, examiné su código fuente. Resultó que la implementación de
forEach()
en fast.js se basa en bucles for. Una implementación rápida del método
forEach()
se vería así:
Una llamada al método
fastForEach()
tres valores:
1 undefined 2
Llamar a
sparseArray.forEach()
solo lleva a la conclusión de dos valores:
1 2
Esta diferencia aparece debido al hecho de que las especificaciones JS con respecto al uso de las funciones de devolución de llamada indican que dichas funciones
no deberían llamarse en índices de matriz remotos o no inicializados (también se denominan "agujeros"). La implementación
fastForEach()
no
fastForEach()
la matriz en busca de agujeros. Esto condujo a un aumento de la velocidad a costa del trabajo correcto con matrices dispersas. Esto fue perfecto para mí, ya que no se utilizaron matrices dispersas en GeoArena.
En este punto, debería tener una prueba rápida en fast.js. Debería instalar la biblioteca, cambiar los métodos estándar del objeto
Array
a métodos de fast.js y probar el rendimiento del juego. Pero en cambio, me moví en una dirección completamente diferente.
Mi desarrollo llamó más rápido.js
El perfeccionista maníaco que vivía en mí quería exprimir absolutamente todo de la optimización del rendimiento del juego. La biblioteca fast.js simplemente no me pareció una solución lo suficientemente buena, ya que su uso implicaba llamar a sus métodos. Entonces pensé: “¿Qué sucede si reemplazo los métodos estándar de matrices simplemente integrando implementaciones nuevas y más rápidas de estos métodos en el código? Eso me ahorraría la necesidad de llamadas al método de la biblioteca ".
Fue esta idea la que me llevó a la ingeniosa idea, que era crear un compilador, al que llamé descaradamente más
rápido.js . Planeaba usarlo en lugar de fast.js. Por ejemplo, aquí está el fragmento de código fuente:
El compilador later.js convertiría este código al siguiente: más rápido, pero peor:
La creación de rapid.js fue impulsada por la misma idea que sustentaba fast.js. Es decir, estamos hablando de microoptimizaciones de rendimiento debido al rechazo de la compatibilidad con matrices dispersas.
A primera vista, rapid.js me pareció un desarrollo extremadamente exitoso. Estos son algunos resultados de un estudio de rendimiento más rápido: js:
array-filter large ✓ native x 232,063 ops/sec ±0.36% (58 runs sampled) ✓ faster.js x 1,083,695 ops/sec ±0.58% (57 runs sampled) faster.js is 367.0% faster (3.386μs) than native array-map large ✓ native x 223,896 ops/sec ±1.10% (58 runs sampled) ✓ faster.js x 1,726,376 ops/sec ±1.13% (60 runs sampled) faster.js is 671.1% faster (3.887μs) than native array-reduce large ✓ native x 268,919 ops/sec ±0.41% (57 runs sampled) ✓ faster.js x 1,621,540 ops/sec ±0.80% (57 runs sampled) faster.js is 503.0% faster (3.102μs) than native array-reduceRight large ✓ native x 68,671 ops/sec ±0.92% (53 runs sampled) ✓ faster.js x 1,571,918 ops/sec ±1.16% (57 runs sampled) faster.js is 2189.1% faster (13.926μs) than native
Los resultados completos de la prueba se pueden encontrar
aquí . Se llevaron a cabo en el Nodo v8.16.1, en el MacBook Pro 2018 de 15 pulgadas.
¿Es mi desarrollo 2000% más rápido que la implementación estándar? Un aumento tan serio en la productividad es, sin duda, algo que puede tener el mayor impacto positivo en cualquier programa. Derecho?
No, no es verdad
Considere un ejemplo simple.
- Imagine que el juego promedio de GeoArena requiere 5,000 milisegundos (ms) de cómputo.
- El compilador más rápido.js acelera la ejecución de los métodos de matriz en un promedio de 10 veces (esta es una estimación aproximada, y también se sobreestima; en la mayoría de las aplicaciones reales ni siquiera hay doble aceleración).
Y aquí está la pregunta que realmente nos interesa: "¿Qué parte de estos 5000 ms se gasta en la implementación de métodos de matriz?".
Supongamos la mitad. Es decir, se gastan 2500 ms en métodos de matriz, los 2500 ms restantes en todo lo demás. Si es así, usar más rápido.js proporcionará un gran aumento de rendimiento.
Ejemplo condicional: el tiempo de ejecución del programa se reduce muchoComo resultado, resulta que el tiempo computacional total se ha reducido en un 45%.
Desafortunadamente, todos estos argumentos están muy, muy lejos de la realidad. GeoArena, por supuesto, utiliza muchos métodos de matriz. Pero la distribución real del tiempo de ejecución del código para diferentes tareas se parece a lo siguiente.
Dura realidadLamentablemente, qué puedo decir.
Este es exactamente el error del que advirtió Donald Knuth. No puse mis esfuerzos en lo que deberían aplicarse, y no lo hice cuando valía la pena hacerlo.
Aquí las matemáticas simples entran en juego. Si algo toma solo el 1% del tiempo de ejecución del programa, entonces optimizarlo dará, en el mejor de los casos, solo un aumento del 1% en la productividad.
Esto es exactamente lo que Donald Knuth tenía en mente cuando dijo "no donde se necesita". Y si piensa en qué "dónde lo necesita", resulta que estas son las partes de los programas que representan los cuellos de botella de rendimiento. Estas son las piezas de código que hacen una contribución significativa al rendimiento general del programa. Aquí el concepto de "productividad" se usa en un sentido muy amplio. Puede incluir el tiempo de ejecución del programa, el tamaño de su código compilado y algo más. Una mejora del 10% en esa parte del programa que afecta en gran medida el rendimiento es mejor que una mejora del 100% en algo pequeño.
Knut también habló de la aplicación de esfuerzos "no cuando sea necesario". El punto de esto es que necesita optimizar algo solo cuando es necesario. Por supuesto, tenía una buena razón para pensar en la optimización. ¿Pero recuerda que comencé a desarrollar fast.js y antes de eso ni siquiera intenté probar la biblioteca fast.js en GeoArena? Los minutos dedicados a probar fast.js en mi juego me ahorrarían semanas de trabajo. Espero que no caigas en la misma trampa en la que yo caí.
Resumen
Si está interesado en experimentar con rapid.js, puede echar un vistazo a
esta demostración. Los resultados que obtenga dependerán de su dispositivo y navegador. Aquí, por ejemplo, lo que sucedió en Chrome 76 en el MacBook Pro 2018 de 15 pulgadas.
Resultados de prueba más rápidos.Puede que le interese conocer los resultados reales del uso de rapid.js en GeoArena. Yo, cuando el juego aún era mío (como dije, lo vendí), realicé una investigación básica. Como resultado, resultó lo siguiente:
- El uso de rapid.js acelera la ejecución del ciclo del juego principal en un juego típico en aproximadamente un 1%.
- Debido al uso de rapid.js, el tamaño del paquete del juego aumentó un 0,3%. Esto ralentizó un poco la carga de la página del juego. El tamaño del paquete ha crecido debido al hecho de que rapid.js convierte el código corto estándar en un código más rápido, pero también más largo.
En general, rapid.js tiene sus pros y sus contras, pero este desarrollo mío no tuvo mucho impacto en el rendimiento de GeoArena. Habría entendido esto mucho antes si me hubiera molestado en probar primero el juego usando fast.js.
Que mi historia te sirva de advertencia.
Estimados lectores! ¿Caíste en la trampa de la optimización prematura?
