paint-brush
El software prospera a menos que lo elimines primero: optimización prematura y una historia de Java GCpor@wasteofserver
549 lecturas
549 lecturas

El software prospera a menos que lo elimines primero: optimización prematura y una historia de Java GC

por Frankie6m2024/04/06
Read on Terminal Reader

Demasiado Largo; Para Leer

No se exceda con las optimizaciones, deje que el lenguaje trabaje para usted. Cuento de historia. Las actualizaciones de Java aumentan el rendimiento de forma gratuita. Punto de referencia, siempre.
featured image - El software prospera a menos que lo elimines primero: optimización prematura y una historia de Java GC
Frankie HackerNoon profile picture
0-item

¿Será una LinkedList más rápida? ¿Debo cambiar el "para cada" con un "iterador"? ¿Debería esta `ArrayList` ser una `Array`? Este artículo surgió en respuesta a una optimización tan malévola que se ha grabado permanentemente en mi memoria.


Antes de abordar Java y las formas de abordar las interferencias, ya sea del recolector de basura o del cambio de contexto, echemos un vistazo a los fundamentos de la escritura de código para su futuro yo.


La optimización prematura es la fuente de todos los males.


Lo has oído antes; La optimización prematura es la fuente de todos los males. Bueno, a veces. Al escribir software, creo firmemente en ser:


  1. lo más descriptivo posible ; debes intentar narrar intenciones como si estuvieras escribiendo una historia.


  2. lo más óptimo posible ; lo que significa que debes conocer los fundamentos del idioma y aplicarlos en consecuencia.

Lo más descriptivo posible

Su código debe expresar su intención, y gran parte de ella se refiere a la forma en que nombra métodos y variables.


 int[10] array1; // bad int[10] numItems; // better int[10] backPackItems; // great

Sólo por el nombre de la variable, ya puedes inferir la funcionalidad.


Si bien numItems es abstracto, backPackItems le dice mucho sobre el comportamiento esperado.


O digamos que tiene este método:


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return new ArrayList<>(0); } // (...) return listOfVisitedCountries; }

En lo que respecta al código, parece más o menos correcto.


¿Podemos hacerlo mejor? ¡Definitivamente podemos!


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return Collections.emptyList(); } // (...) return listOfVisitedCountries; }

Leer Collections.emptyList() es mucho más descriptivo que new ArrayList<>(0);


Imagina que estás leyendo el código anterior por primera vez y te topas con la cláusula de protección que verifica si el usuario realmente ha visitado países. Además, imagina que esto está enterrado en una clase larga, leer Collections.emptyList() es definitivamente más descriptivo que new ArrayList<>(0) , también te estás asegurando de que sea inmutable y que el código del cliente no pueda modificarlo.

Lo más óptimo posible

Conozca su idioma y utilícelo en consecuencia. Si necesita un double , no es necesario envolverlo en un objeto Double . Lo mismo ocurre con el uso de List si todo lo que realmente necesitas es un Array .


Sepa que debe concatenar cadenas usando StringBuilder o StringBuffer si comparte el estado entre subprocesos:


 // don't do this String votesByCounty = ""; for (County county : counties) { votesByCounty += county.toString(); } // do this instead StringBuilder votesByCounty = new StringBuilder(); for (County county : counties) { votesByCounty.append(county.toString()); }


Sepa cómo indexar su base de datos. Anticípese a los cuellos de botella y almacene en caché en consecuencia. Todo lo anterior son optimizaciones. Son el tipo de optimizaciones que ustedes deben conocer e implementar como primeros ciudadanos.

¿Cómo lo matas primero?

Nunca olvidaré un truco que leí hace un par de años. A decir verdad, el autor retrocedió rápidamente, pero esto demuestra cómo mucho mal puede surgir de buenas intenciones.


 // do not do this, ever! int i = 0; while (i<10000000) { // business logic if (i % 3000 == 0) { //prevent long gc try { Thread.sleep(0); } catch (Ignored e) { } } }

¡Un truco recolector de basura del infierno!


Puedes leer más sobre por qué y cómo funciona el código anterior en el artículo original y, si bien el exploit es definitivamente interesante, esta es una de esas cosas que nunca deberías hacer.


  • Funciona mediante efectos secundarios, Thread.sleep(0) no tiene ningún propósito en este bloque
  • Funciona explotando una deficiencia de código en sentido descendente
  • Para cualquiera que herede este código, es oscuro y mágico.


Solo comience a forjar algo un poco más complicado si, después de escribir con todas las optimizaciones predeterminadas que proporciona el lenguaje , se topó con un cuello de botella. Pero manténgase alejado de brebajes como el anterior.


Una interpretación del futuro recolector de basura de Java "imaginado" por Microsoft Copilot


¿Cómo abordar a ese recolector de basura?

Si después de todo, el recolector de basura sigue siendo la pieza que ofrece resistencia, estas son algunas de las cosas que puedes probar:


  • Si su servicio es tan sensible a la latencia que no puede permitir GC, ejecútelo con "Epsilon GC" y evite GC por completo .
    -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC


    Obviamente, esto hará crecer su memoria hasta que obtenga una excepción OOM, por lo que es un escenario de corta duración o su programa está optimizado para no crear objetos.


  • Si su servicio es algo sensible a la latencia, pero la tolerancia permitida permite cierto margen de maniobra , ejecute GC1 y aliméntelo con algo como -XX:MaxGCPauseTimeMillis=100 (el valor predeterminado es 250 ms).

  • Si el problema surge de bibliotecas externas , digamos que una de ellas llama a System.gc() o Runtime.getRuntime().gc() , que son recolectores de basura que detienen el mundo, puede anular el comportamiento ofensivo ejecutando con -XX:+DisableExplicitGC



INICIO DE VERSIÓN

FIN DE VERSIÓN

GC PREDETERMINADO

Java 1

java 4

Recolector de basura en serie

java 5

Java 8

Recolector de basura paralelo

Java 9

en curso

Recolector de basura G1


Nota 1: desde Java 15, ZGC está listo para producción , pero aún debe activarlo explícitamente con -XX:+UseZGC .


Nota 2: La VM considera las máquinas como de clase servidor si detecta más de dos procesadores y un tamaño de montón mayor o igual a 1792 MB. Si no es de clase de servidor, por defecto será Serial GC .


En esencia, opte por el ajuste de GC cuando esté claro que las limitaciones de rendimiento de la aplicación están directamente relacionadas con el comportamiento de recolección de basura y tenga la experiencia necesaria para realizar ajustes informados. De lo contrario, confíe en la configuración predeterminada de la JVM y concéntrese en optimizar el código a nivel de aplicación.

u/shiphe - querrás leer el comentario completo


Otras bibliotecas relevantes que quizás desee explorar:

Arnés de microbenchmark de Java (JMH)

Si está optimizando sin tener una evaluación comparativa real, no se está haciendo ningún favor. JMH es la biblioteca Java de facto para probar el rendimiento de sus algoritmos. Úselo.

Afinidad de subprocesos de Java

Fijar un proceso a un núcleo específico puede mejorar los accesos a la caché. Dependerá del hardware subyacente y de cómo su rutina maneja los datos. No obstante, esta biblioteca hace que sea tan fácil de implementar que, si un método que requiere un uso intensivo de la CPU lo está arrastrando, querrá probarlo.

Disruptor LMAX

Esta es una de esas bibliotecas que, aunque no la necesites, querrás estudiar. La idea es permitir una simultaneidad de latencia ultrabaja. Pero la forma en que se implementa, desde la simpatía mecánica hasta el buffer circular, aporta muchos conceptos nuevos. Todavía recuerdo cuando lo descubrí por primera vez, hace siete años, pasando toda la noche para digerirlo.

terremoto de Netflix

La premisa de jvmquake es que cuando las cosas van mal con la JVM, desea que muera y no se cuelgue. Hace un par de años, estaba ejecutando simulaciones en un clúster HTCondor que tenía limitaciones de memoria estrictas y, a veces, los trabajos se atascaban debido a errores de "falta de memoria".


Esta biblioteca obliga a la JVM a morir, lo que le permite solucionar el error real. En este caso específico, HTCondor reprogramaría automáticamente el trabajo.

Pensamientos finales

¿El código que me hizo escribir esta publicación? He escrito mucho peor. Todavía lo hago. Lo mejor que podemos esperar es cometer cada vez menos errores.


Espero estar descontento al ver mi propio código dentro de unos años.


Y esa es una buena señal.



Ediciones y gracias:


También publicado en Wasteofserver.com.