El término "escala web" se ha promocionado recientemente, y las personas también están haciendo que sus sistemas sean más "dominios amplios" al expandir su arquitectura de aplicaciones. Pero, ¿qué es exactamente un dominio completo? ¿O cómo garantizar todo el dominio?
La carga de escala más publicitada es el dominio más popular. Por ejemplo, los sistemas que admiten acceso de usuario único también pueden admitir 10, 100 o incluso 1 millón de accesos de usuario. Idealmente, nuestro sistema debe permanecer lo más "apátrido" posible. Incluso si debe existir un estado, se puede convertir y transmitir en diferentes terminales de procesamiento de la red. Cuando la carga se convierte en un cuello de botella, puede que no haya retraso. Entonces, para una sola solicitud, es aceptable tomar de 50 a 100 milisegundos. Esto se llama escalar.
La extensión funciona de manera completamente diferente en la optimización completa del dominio. Por ejemplo, un algoritmo que garantiza que un datos se procese con éxito también puede procesar con éxito 10, 100 o incluso 1 millón de datos. La complejidad del evento (símbolos O grandes) es la mejor descripción independientemente de si este tipo métrico es factible o no. La latencia es un asesino de escala de rendimiento. Hará todo lo posible para procesar todas las operaciones en la misma máquina. Esto se llama escalar.
Si el pastel puede caer del cielo (por supuesto, esto es imposible), podemos combinar la expansión horizontal y la expansión vertical. Sin embargo, hoy solo vamos a introducir las siguientes formas simples de mejorar la eficiencia.
Tanto Forkjoinpool en Java 7 como los flujos de datos paralelos en Java 8 (ParallelStream) son útiles para el procesamiento paralelo. Es particularmente obvio al implementar programas Java en procesadores de múltiples núcleos, ya que todos los procesadores pueden acceder a la misma memoria.
Por lo tanto, el beneficio fundamental de este procesamiento paralelo es eliminar casi por completo la latencia en comparación con la escala en diferentes máquinas en todas las redes.
¡Pero no se confunda con los efectos del procesamiento paralelo! Tenga en cuenta los siguientes dos puntos:
La reducción de la complejidad del algoritmo es, sin duda, la forma más efectiva de mejorar el rendimiento. Por ejemplo, para un método de búsqueda de instancia hashmap (), la complejidad de eventos o (1) o la complejidad del espacio o (1) son los más rápidos. Pero esta situación a menudo es imposible, y mucho menos logra fácilmente.
Si no puede reducir la complejidad del algoritmo, también puede mejorar el rendimiento al encontrar puntos clave en el algoritmo y mejorar los métodos. Supongamos que tenemos el siguiente diagrama de algoritmo:
La complejidad del tiempo general de este algoritmo es O (N3), y si se calcula en un orden de acceso separado, también se puede concluir que la complejidad es O (n x o x p). Pero de todos modos, encontraremos algunos escenarios extraños cuando analicemos este código:
Sin referencia de datos de producción, podemos sacar conclusiones fácilmente de que debemos optimizar las "operaciones de alto nivel". Pero las optimizaciones que hicimos no tuvieron ningún efecto en los productos entregados.
La regla de oro optimizada no es más que la siguiente:
Eso es todo para la teoría. Suponiendo que hemos encontrado que el problema ocurre en la rama correcta, es probable que el procesamiento simple en el producto pierda respuesta debido a una gran cantidad de tiempo (suponiendo que los valores de N, O y P sean muy grandes), tenga en cuenta que la complejidad del tiempo de la rama izquierda mencionada en el artículo es O (N3). Los esfuerzos realizados aquí no se pueden extender, pero pueden ahorrar tiempo para los usuarios y retrasar las mejoras de rendimiento difíciles hasta más tarde.
Aquí hay 10 consejos para mejorar el rendimiento de Java:
1. Use StringBuilder
StingBuilder debe usarse de forma predeterminada en nuestro código Java, y el operador + debe evitarse. Tal vez tenga diferentes opiniones sobre el azúcar de sintaxis de StringBuilder, como:
Cadena x = "a" + args.length + "b";
Se compilará como:
0 nuevo java.lang.stringbuilder [16] 3 dup4 ldc <string "a"> [18] 6 invokespecial java.lang.stringbuilder (java.lang.string) [20] 9 Aload_0 [Args] 10 Arraylength11 Intovirtual Java.lang.StringBuilder.append (int): JAVELANT [23] 14 LDC <String "B"> [27] 16 InvokeVirtual java.lang.stringbuilder.append (java.lang.string): java.lang.stringbuilder [29] 19 InvokeVirtual java.lang.stringbuilder.ToString (): java.lang.string [32] 22 store_1 [x] [x]
Pero, ¿qué pasó exactamente? ¿Necesita usar las siguientes secciones para mejorar la cadena a continuación?
Cadena x = "a" + args.length + "b"; if (args.length == 1) x = x + args [0];
Ahora hemos usado el segundo StringBuilder, que no consume memoria adicional en el montón, pero ejerce presión sobre el GC.
StringBuilder x = new StringBuilder ("A"); X.Append (args.length); x.append ("b"); if (args.length == 1); x.append (args [0]);En el ejemplo anterior, si confía en el compilador Java para generar implícitamente la instancia, el efecto compilado no tiene nada que ver con si se utiliza la instancia de StringBuilder. Recuerde: en la sucursal NOPE, el tiempo dedicado a cada ciclo de la CPU se gasta en GC o asignando el espacio predeterminado para StringBuilder, y estamos desperdiciando el tiempo N x o x p.
En términos generales, usar StringBuilder es mejor que usar el operador +. Si es posible, seleccione StringBuilder si necesita aprobar referencias en múltiples métodos, porque String consume recursos adicionales. JoOQ utiliza este método al generar declaraciones SQL complejas. Solo se usa un StringBuilder durante todo el paso de SQL del árbol de sintaxis abstracto.
Lo que es aún más trágico es que si todavía está usando StringBuffer, use StringBuilder en lugar de StringBuffer. Después de todo, realmente no hay muchos casos en los que las cadenas necesiten ser sincronizadas.
Las expresiones regulares dan a las personas la impresión de que son rápidas y fáciles. Pero el uso de expresiones regulares en la sucursal no sería la peor decisión. Si tiene que usar expresiones regulares en código intensivo de cómputo, al menos almacena en caché el patrón para evitar compilar repetidamente el patrón.
Patrón final estático Heavy_regex = Pattern.Compile ("((((x)*y)*z)*");Si solo se usa una expresión regular simple como la siguiente:
Cadena [] piezas = ipaddress.split ("//.");Esto es mejor usar una matriz de char [] normal o una operación basada en índices. Por ejemplo, el siguiente código con mala legibilidad en realidad juega el mismo papel.
int longitud = ipaddress.length (); int offset = 0; int part = 0; for (int i = 0; i <longitud; i ++) {if (i == longitud - 1 || iPaddress.charat (i+1) == '.') {Parts [par] = ipaddress.substring (offset, i+1); parte ++; offset = i+2;}El código anterior también muestra que la optimización prematura no tiene sentido. Aunque se compara con el método Split (), este código es menos mantenible.
Desafío: ¿Pueden los amigos inteligentes encontrar algoritmos más rápidos?
Las expresiones regulares son muy útiles, pero también tienen que pagar un precio cuando se usan. Especialmente cuando estás en las sucursales de Nout, debes evitar usar expresiones regulares a toda costa. También tenga cuidado con varios métodos de cadena JDK que usan expresiones regulares, como String.replaceall () o String.split (). Puede elegir utilizar una biblioteca de desarrollo más popular, como Apache Commons Lang, para realizar operaciones de cadena.
Esta sugerencia no se aplica a las ocasiones generales, sino solo a los escenarios profundos en la rama no. Aun así, debes entender algo. El método de escritura de bucle de formato Java 5 es tan conveniente que podemos olvidar el método de bucle interno, como:
for (valor de cadena: cadenas) {// haz algo útil aquí}Cada vez que el código se ejecuta en este bucle, si la variable de cadenas es iterable, el código creará automáticamente una instancia del iterador. Si está utilizando ArrayList, la máquina virtual asignará automáticamente 3 memoria de tipo entero al objeto en el montón.
clase privada ITR implementa iterador <E> {int cursor; int lastret = -1; int esperadoModCount = modCount; // ...También puede usar el siguiente método de bucle equivalente para reemplazar el anterior para el bucle, solo "desperdiciando" un trozo de cirugía plástica en la pila, que es bastante rentable.
int size = strings.size (); para (int i = 0; i <size; i ++) {valor de cadena: strings.get (i); // Haz algo útil aquí}Si el valor de la cadena en el bucle no cambia mucho, también puede usar matrices para implementar el bucle.
for (valor de cadena: StringArray) {// Haz algo útil aquí}Ya sea desde la perspectiva de la lectura y la escritura fácil, o desde la perspectiva del diseño de API, los iteradores, las interfaces iterables y los bucles foreach son muy útiles. Pero al costo, al usarlos, se crea un objeto adicional en el montón para cada niño de bucle. Si el bucle se ejecutará muchas veces, tenga cuidado de evitar generar instancias sin sentido. Es mejor usar el método básico de bucle de puntero para reemplazar el iterador mencionado anteriormente, la interfaz iterable y el bucle foreach.
Algunas opiniones que se oponen a lo anterior (especialmente reemplazando a los iteradores con punteros) se discuten en Reddit para más detalles.
Algunos métodos son caros. Tomando la rama NOPE como ejemplo, no mencionamos los métodos relevantes de hojas, pero este se puede encontrar. Supongamos que nuestro controlador JDBC debe superar todas las dificultades para calcular el valor de retorno del método resultset.wasnull (). El marco SQL que implementamos puede verse así:
if (type == Integer.class) {resultado = (t) wasnull (rs, integer.valueOf (rs.getInt (index)));} // y luego ... estático final <t> t wasnull (resultset rs, value) tira sqlexception {return rs.wasnull ()? nulo: valor;}En la lógica anterior, el método resultset.wasnull () se llama cada vez que el valor int se obtiene del conjunto de resultados, pero el método de getInt () se define como:
Tipo de retorno: valor variable; Si el resultado de la consulta SQL es nulo, regrese 0.
Por lo tanto, una forma simple y efectiva de mejorarlo es la siguiente:
static final <t extiende el número> t wasnull (resultset rs, t value) lanza SQLException {return (value == null || (value.intValue () == 0 && rs.wasnull ()))? nulo: valor;}Esto es una brisa.
El método de caché llama para reemplazar los altos métodos de sobrecarga en los nodos de la hoja, o evitar llamar a los métodos de alto nivel si la convención del método lo permite.
Lo anterior introduce el uso de una gran cantidad de genéricos en el ejemplo de Jooq, lo que resulta en el uso de clases de byte, Short, int y largas. Pero al menos los genéricos no deben ser una limitación del código hasta que estén especializados en proyectos Java 10 o Valhalla. Porque puede ser reemplazado por el siguiente método:
// almacenado en el entero del montón i = 817598;
... si escribes de esta manera:
// almacenar en la pila int i = 817598;
Las cosas pueden empeorar al usar matrices:
// se generaron tres objetos en el entero de almacenamiento [] i = {1337, 424242};... si escribes de esta manera:
// Solo se genera un objeto en el montón int [] i = {1337, 424242};Cuando estamos profundamente en la sucursal de NOPE, debemos hacer todo lo posible para evitar usar clases de embalaje. La desventaja de hacer esto es que ejerce mucha presión sobre GC. GC estará muy ocupado para borrar los objetos generados por la clase de embalaje.
Por lo tanto, un método de optimización efectivo es usar tipos de datos básicos, matrices de longitud fija y usar una serie de variables divididas para identificar la posición del objeto en la matriz.
Trove4J, que sigue el protocolo LGPL, es una biblioteca de clases de colección Java, que nos proporciona una mejor implementación de rendimiento que la matriz de conformación int [].
La siguiente excepción es para esta regla: porque los tipos booleanos y de bytes no son suficientes para permitir que JDK proporcione un método de caché para ello. Podemos escribir de esta manera:
Booleano a1 = verdadero; // ... sintaxis azúcar para: boolean a2 = boolean.valueof (true); byte b1 = (byte) 123; // ... Azúcar sintaxis para: byte b2 = byte.valueOf ((byte) 123);
Otros tipos básicos de enteros también tienen situaciones similares, como Char, Short, Int y Long.
No encerre automáticamente estos tipos primitivos enteros al llamar a los constructores ni llame al método thetype.valueOf ().
Tampoco llame a constructores en las clases de envoltura a menos que desee obtener una instancia que no se cree en el montón. La ventaja de hacer esto es darle una gran broma del Día de los Inocentes a sus colegas.
Por supuesto, si desea experimentar la biblioteca de funciones fuera del montón, aunque esto puede mezclarse con muchas decisiones estratégicas, en lugar de la solución local más optimista. Para obtener un artículo interesante sobre el almacenamiento que no es del almacenamiento escrito por Peter Lawrey y Ben Cotton, haga clic en: OpenJDK y Hashmap-Deje que los veteranos dominen las nuevas técnicas de forma segura (¡almacenamiento sin lavado!).
Hoy en día, los lenguajes de programación funcional como Scala fomentan la recursión. Porque la recursión generalmente significa recuperación de la cola que puede descomponerse en individuos optimizados individualmente. Sería mejor si el lenguaje de programación que usa puede admitirlo. Pero aun así, tenga cuidado de que el ligero ajuste del algoritmo convierta la recursión de la cola en la recursión ordinaria.
Esperemos que el compilador pueda detectar esto automáticamente, de lo contrario habríamos desperdiciado muchos marcos de pila para cosas que se pueden hacer con solo unas pocas variables locales.
No hay nada que decir en esta sección, excepto que iterando tanto como sea posible en la rama NOPE en lugar de la recursión.
Cuando queremos iterar sobre un mapa guardado en pares de valores clave, debemos encontrar una buena razón para el siguiente código:
para (k k key: map.keySet ()) {V valor: map.get (clave);}Sin mencionar el siguiente método de escritura:
para (Entry <k, v> Entry: map.Entryset ()) {k key = entry.getKey (); V value = entry.getValue ();}Cuando usamos la rama NOPE, debemos usar el mapa con precaución. Porque muchas operaciones de acceso que parecen tener una complejidad de tiempo de O (1) en realidad están compuestas por una serie de operaciones. Y el acceso en sí no es gratuito. ¡Al menos, si tiene que usar MAP, debe usar el método EntrySet () para iterar! De esta manera, solo queremos acceder a una instancia de map.entry.
Cuando sea necesario iterar sobre el mapa en forma de pares de valor clave, asegúrese de usar el método EntrySet ().
8. Use enumset o enummap
En algunos casos, como cuando se usa un mapa de configuración, podemos saber de antemano el valor clave guardado en el mapa. Si este valor clave es muy pequeño, debemos considerar usar enumset o enummap en lugar de usar el hashset o el hashmap que comúnmente usamos. El siguiente código proporciona una explicación clara:
objeto transitorio privado [] vals; public v put (k key, V value) {// ... int index = key.ordinal (); vals [index] = MaskNull (valor); // ...}La implementación clave del código anterior es que usamos matrices en lugar de tablas hash. Especialmente al insertar nuevos valores en el mapa, todo lo que tiene que hacer es obtener un número de secuencia constante generado por el compilador para cada tipo de ENUM. Si hay una configuración de mapa global (por ejemplo, solo una instancia), Enummap logrará un rendimiento más sobresaliente que HASHMAP bajo la presión de aumentar la velocidad de acceso. La razón es que Enummap usa una memoria de montón de bit menos que HASHMAP, y HashMap llama al método hashcode () y el método igual () en cada valor de clave.
Enum y enummap son amigos cercanos. Cuando usamos valores clave similares a las estructuras similares a Enum, debemos considerar declarar estos valores clave como tipos de enum y usarlos como claves de enumma.
En el caso de que no se pueda utilizar enummap, al menos los métodos hashcode () e igual () deben optimizarse. Es necesario un buen método hashcode () porque evita llamadas innecesarias al método igual () iguales ().
En la estructura de herencia de cada clase, se necesitan objetos simples que sean fácilmente aceptables. Echemos un vistazo a cómo se implementa la Tabla de Jooq's Org.Jooq.Table.
El método de implementación hashcode () más simple y más rápido es el siguiente:
// AbstractTable Una implementación básica de una tabla general: @OverridePublic int hashcode () {// [#1938] En comparación con la consulta estándar, esta es una implementación hashcode () más eficiente. Return name.hashCode ();}El nombre es el nombre de la tabla. Ni siquiera necesitamos considerar el esquema u otros atributos de la tabla, porque los nombres de las tabla suelen ser únicos en la base de datos. Y el nombre de la variable es una cadena, que ya ha almacenado en caché un valor hashcode ().
Los comentarios en este código son muy importantes porque la mesa abstracta heredada de AbstractQueryPart es la implementación básica de cualquier elemento de árbol de sintaxis abstracto. Los elementos de árbol de sintaxis abstractos ordinarios no tienen ningún atributo, por lo que no pueden tener ninguna fantasía sobre optimizar la implementación del método hashcode (). El método hashcode () sobrescrito es el siguiente:
// AbstractQueryPart Una implementación básica de árbol de sintaxis abstracto general: @OverridePublic int hashcode () {// Esta es una implementación predeterminada de funcionamiento. // La subclase de implementación específica debe anular este método para mejorar el rendimiento. return create (). renderInlined (this) .hashcode ();}En otras palabras, todo el flujo de trabajo de representación SQL se activa para calcular el código hash para un elemento de árbol de sintaxis abstracto normal.
El método igual () es aún más interesante:
// Implementación básica de la tabla general de la Tabla Abstract: @OverridePublic Boolean iguales (Object that) {if (this == que) {return true;} // [#2144] antes de llamar al método abstractQuererypart.equals (), // puede saber si los objetos no son iguales. if (esa instancia de abstractTable) {if (stringUtils.equals (name, (((abstractTable <?>) that) .name)))) {return super.equals (que);} return false;} return false;}Primero, no use el método Equals () demasiado temprano (no solo en no), si:
Nota: Si usamos instancia de Too Beat para verificar los tipos compatibles, las siguientes condiciones incluyen argumento == NULL. Ya expliqué esto en mi blog anterior, consulte 10 mejores prácticas de codificación de Java.
Después de que termine nuestra comparación de las situaciones anteriores, deberíamos poder sacar algunas conclusiones. Por ejemplo, el método table.equals () de Jooq se usa para comparar si las dos tablas son las mismas. Independientemente del tipo de implementación específico, deben tener el mismo nombre de campo. Por ejemplo, los siguientes dos elementos no pueden ser los mismos:
Si podemos determinar fácilmente si el parámetro entrante es igual a la instancia en sí (esto), podemos renunciar a la operación si el resultado es falso. Si el resultado de la devolución es verdadero, podemos juzgar aún más la súper implementación de la clase principal. En el caso de que la mayoría de los objetos comparados no sean iguales, podemos finalizar el método lo antes posible para ahorrar tiempo de ejecución de CPU.
Algunos objetos tienen mayor similitud que otros.
En Jooq, la mayoría de las instancias de tabla son generadas por el generador de código de Jooq, y el método Equals () de estas instancias ha sido profundamente optimizado. Docenas de otros tipos de tabla (tablas derivadas), funciones con valor de tabla, tablas de matriz, tablas unidas, tablas de pivote, expresiones de tabla comunes, etc., mantienen la implementación básica del método igual ().
Finalmente, hay otra situación que se puede aplicar a todos los idiomas y no solo a Java. Además, la rama NOPE que estudiamos anteriormente también será útil para comprender de O (N3) a O (N log n).
Desafortunadamente, muchos programadores usan algoritmos simples y locales para considerar el problema. Están acostumbrados a resolver problemas paso a paso. Esta es la forma "sí/o" de imperativo. Este estilo de programación es fácil de modelar "imagen más grande" al convertir de programación imperativa pura a programación orientada a objetos a programación funcional, pero estos estilos carecen de lo que solo existe en SQL y R:
Programación declarativa.
En SQL, podemos declarar el efecto requerido para la base de datos sin considerar la influencia del algoritmo. La base de datos puede adoptar el mejor algoritmo de acuerdo con el tipo de datos, como restricciones, claves, índices, etc.
En teoría, primero tuvimos ideas básicas después de SQL y cálculos relacionales. En la práctica, los proveedores de SQL han implementado optimizadores eficientes basados en gastos generales (optimizadores basados en costos) en las últimas décadas. Luego, en la versión 2010, finalmente descubrimos todo el potencial de SQL.
Pero no necesitamos implementar SQL utilizando el método SET. Todos los idiomas y bibliotecas admiten conjuntos, colecciones, bolsas y listas. El principal beneficio de usar SET es que puede hacer que nuestro código sea conciso y claro. Por ejemplo, el siguiente método de escritura:
Someset se cruza de SomeThomset
En cambio
// El método de escritura anterior de Java 8 establece resultado = new Hashset (); para (candidato de objeto: someset) if (someThset.contains (candidato)) resultado.Add (candidato); // Incluso si se usa Java 8, no es muy útil.someset.stream (). Filtrar (SomeTherset :: contiene) .Collect (Collectors.Toset ());
Algunas personas pueden tener opiniones diferentes sobre la programación funcional y Java 8 que pueden ayudarnos a escribir algoritmos más simples y simples. Pero este punto de vista no es necesariamente cierto. Podemos convertir el imperativo bucle Java 7 en la colección de transmisión de Java 8, pero aún usamos el mismo algoritmo. Pero las expresiones de estilo SQL son diferentes:
Someset se cruza de SomeThomsetEl código anterior puede tener 1000 implementaciones diferentes en diferentes motores. Lo que estamos estudiando hoy es convertir automáticamente dos conjuntos en Enumset antes de llamar a la operación de intersección. Incluso podemos realizar operaciones intersectas paralelas sin llamar al método subyacente stream.parallel ().
En este artículo, discutimos las optimizaciones sobre la ramificación de NOPE. Por ejemplo, profundo en algoritmos de alta complejidad. Como desarrolladores de Jooq, nos complace optimizar la generación SQL.
Jooq está en la "parte inferior de la cadena alimentaria" porque es la última API llamada por nuestro programa de computadora al dejar el JVM y ingresar a DBMS. Ubicado en la parte inferior de la cadena alimentaria, significa que cualquier línea lleva el tiempo N x o x p cuando se ejecuta en Jooq, por lo que quiero optimizarla lo antes posible.
Nuestra lógica comercial puede no ser tan complicada como la sucursal de NOPE. Sin embargo, el marco básico puede ser muy complejo (marco local de SQL, bibliotecas locales, etc.). Por lo tanto, debemos seguir los principios mencionados hoy, usar el control de la misión Java u otras herramientas para verificar si hay algún área que necesite optimización.
Enlace original: Traducción de Jaxenter: importnew.com - siempre en el enlace de traducción de la carretera: http://www.importnew.com/16181.html