Le terme «échelle Web» a été révolue récemment, et les gens rendent également leurs systèmes plus «domaine large» en élargissant leur architecture d'application. Mais qu'est-ce qu'un domaine complet? Ou comment assurer l'ensemble du domaine?
La charge de mise à l'échelle la plus excitée est le domaine le plus populaire. Par exemple, les systèmes qui prennent en charge l'accès des utilisateurs uniques peuvent également prendre en charge 10, 100 ou même 1 million d'accès utilisateurs. Idéalement, notre système devrait rester le plus «apatride» que possible. Même si un état doit exister, il peut être converti et transmis sur différentes bornes de traitement du réseau. Lorsque la charge devient un goulot d'étranglement, il ne peut y avoir de retard. Donc, pour une seule demande, il est acceptable de prendre 50 à 100 millisecondes. C'est ce qu'on appelle la mise à l'échelle.
L'extension fonctionne complètement différemment dans l'optimisation complète du domaine. Par exemple, un algorithme qui garantit que les données sont traitées avec succès peuvent également traiter avec succès 10, 100 ou même 1 million de données. La complexité des événements (grands symboles O) est la meilleure description, que ce type de métrique soit possible ou non. La latence est un tueur à l'échelle des performances. Vous faites de votre mieux pour traiter toutes les opérations sur la même machine. C'est ce qu'on appelle la mise à l'échelle.
Si la tarte peut tomber du ciel (bien sûr, cela est impossible), nous pouvons être en mesure de combiner l'expansion horizontale et l'expansion verticale. Cependant, aujourd'hui, nous allons uniquement introduire les moyens simples suivants d'améliorer l'efficacité.
Forkjoinpool dans Java 7 et les flux de données parallèles dans Java 8 (Parallelstream) sont utiles pour le traitement parallèle. Il est particulièrement évident lors du déploiement de programmes Java sur des processeurs multi-core, car tous les processeurs peuvent accéder à la même mémoire.
Par conséquent, l'avantage fondamental de ce traitement parallèle est d'éliminer presque complètement la latence par rapport à la mise à l'échelle sur différentes machines à travers les réseaux.
Mais ne soyez pas confus par les effets du traitement parallèle! Veuillez garder à l'esprit les deux points suivants:
La réduction de la complexité des algorithmes est sans aucun doute le moyen le plus efficace d'améliorer les performances. Par exemple, pour une méthode de recherche d'instance HashMap (), la complexité des événements O (1) ou la complexité de l'espace O (1) sont les plus rapides. Mais cette situation est souvent impossible, et encore moins facilement.
Si vous ne pouvez pas réduire la complexité de l'algorithme, vous pouvez également améliorer les performances en trouvant des points clés dans l'algorithme et en améliorant les méthodes. Supposons que nous ayons le diagramme d'algorithme suivant:
La complexité temporelle globale de cet algorithme est O (N3), et si elle est calculée dans un ordre d'accès séparé, il peut également être conclu que la complexité est O (n x o x p). Mais de toute façon, nous trouverons des scénarios étranges lorsque nous analyserons ce code:
Sans référence de données de production, nous pouvons facilement tirer des conclusions selon lesquelles nous devons optimiser les "opérations élevées". Mais les optimisations que nous avons faites n'ont eu aucun effet sur les produits livrés.
La règle d'or optimisée n'est rien de plus que ce qui suit:
C’est tout pour la théorie. En supposant que nous avons constaté que le problème se produit sur la branche de droite, il est probable que le traitement simple du produit perd une réponse en raison d'une grande quantité de temps (en supposant que les valeurs de N, O et P sont très importantes), veuillez noter que la complexité temporelle de la branche gauche mentionnée dans l'article est O (N3). Les efforts déployés ici ne peuvent pas être prolongés, mais peuvent gagner du temps pour les utilisateurs et retarder les améliorations difficiles des performances que plus tard.
Voici 10 conseils pour améliorer les performances de Java:
1. Utilisez StringBuilder
Stingbuilder doit être utilisé par défaut dans notre code Java, et l'opérateur + doit être évité. Peut-être que vous aurez des opinions différentes sur le sucre de syntaxe de StringBuilder, comme:
String x = "a" + args.length + "b";
Sera compilé comme:
0 new Java.lang.stringBuilder [16] 3 dup4 ldc <string "a"> [18] 6 invokepecial java.lang.stringbuilder (java.lang.string) [20] 9 aload_0 [args] 10 ArrayLength11 java.lang.StringBuilder [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]
Mais que s'est-il passé exactement? Avez-vous besoin d'utiliser les sections suivantes pour améliorer la chaîne ensuite?
String x = "a" + args.length + "b"; if (args.length == 1) x = x + args [0];
Maintenant, nous avons utilisé le deuxième stringbuilder, qui ne consomme pas de mémoire supplémentaire dans le tas, mais met la pression sur le GC.
StringBuilder x = new StringBuilder ("A"); x.append (args.length); x.append ("b"); if (args.length == 1); x.append (args [0]);Dans l'exemple ci-dessus, si vous comptez sur le compilateur Java pour générer implicitement l'instance, l'effet compilé n'a rien à voir si l'instance StringBuilder est utilisée. N'oubliez pas: Dans la branche Nope, le temps passé sur chaque cycle CPU est passé sur GC ou allouant l'espace par défaut pour STRINGBUILDER, et nous perdons du temps N X O X P.
D'une manière générale, l'utilisation de StringBuilder est meilleure que d'utiliser l'opérateur +. Si possible, sélectionnez StringBuilder Si vous devez transmettre des références sur plusieurs méthodes, car String consomme des ressources supplémentaires. JOOQ utilise cette méthode lors de la génération d'instructions SQL complexes. Un seul STRACHBUILDER est utilisé pendant l'ensemble du passage SQL de l'arborescence de syntaxe abstraite.
Ce qui est encore plus tragique, c'est que si vous utilisez toujours StringBuffer, utilisez StringBuilder au lieu de StringBuffer. Après tout, il n'y a vraiment pas beaucoup de cas où les chaînes doivent être synchronisées.
Les expressions régulières donnent aux gens l'impression qu'ils sont rapides et faciles. Mais utiliser des expressions régulières dans la branche Nope serait la pire décision. Si vous devez utiliser des expressions régulières dans un code à forte intensité de calcul, cachez au moins le motif pour éviter la compilation à plusieurs reprises du motif.
STATIC FINAL Pattern Heavy_Regex = Pattern.Compile ("((((x) * y) * z) *");Si seulement une expression régulière simple comme ce qui suit est utilisée:
String [] Parts = iPaddress.split ("//.");Il est préférable d'utiliser un tableau de char [] normal ou une opération basée sur l'indice. Par exemple, le code suivant avec une mauvaise lisibilité joue en fait le même rôle.
int longueur = ipaddress.length (); int offset = 0; int part = 0; for (int i = 0; i <longueur; i ++) {if (i == longueur - 1 || iPaddress.charat (i + 1) == '.Le code ci-dessus montre également que l'optimisation prématurée est dénuée de sens. Bien que par rapport à la méthode Split (), ce code est moins maintenable.
Défi: Smart Friends peut-il proposer des algorithmes plus rapides?
Les expressions régulières sont très utiles, mais ils doivent également payer un prix lorsqu'ils sont utilisés. Surtout lorsque vous êtes profondément dans les branches non, vous devez éviter d'utiliser à tout prix des expressions régulières. Faites également attention à diverses méthodes JDK String qui utilisent des expressions régulières, telles que String.ReplaceAll () ou String.Split (). Vous pouvez choisir d'utiliser une bibliothèque de développement plus populaire, comme Apache Commons Lang, pour effectuer des opérations de chaîne.
Cette suggestion ne s'applique pas aux occasions générales, mais uniquement à des scénarios profondément dans la branche Nope. Même ainsi, vous devez comprendre quelque chose. La méthode d'écriture de boucle de format Java 5 est si pratique que nous pouvons oublier la méthode de boucle interne, telle que:
pour (valeur de chaîne: chaînes) {// faire quelque chose d'utile ici}Chaque fois que le code se déroule dans cette boucle, si la variable de chaînes est un itérable, le code créera automatiquement une instance de l'itérateur. Si vous utilisez ArrayList, la machine virtuelle allouera automatiquement la mémoire de type entier à l'objet sur le tas.
classe privée ITR implémente Iterator <E> {int Cursor; int lastret = -1; int attendModCount = modCount; // ...Vous pouvez également utiliser la méthode de boucle équivalente suivante pour remplacer la boucle ci-dessus, "gaspiller" un morceau de chirurgie plastique sur la pile, ce qui est assez rentable.
int size = Strings.size (); pour (int i = 0; i <size; i ++) {String Value: Strings.get (i); // faire quelque chose d'utile ici}Si la valeur de la chaîne dans la boucle ne change pas beaucoup, vous pouvez également utiliser des tableaux pour implémenter la boucle.
for (String Value: StringArray) {// faire quelque chose d'utile ici}Que ce soit du point de vue de la lecture et de l'écriture faciles, ou du point de vue de la conception de l'API, les itérateurs, les interfaces itératives et les boucles Forach sont très utiles. Mais au prix, lors de leur utilisation, un objet supplémentaire est créé sur le tas pour chaque enfant en boucle. Si la boucle doit être exécutée plusieurs fois, veillez à éviter de générer des instances dénuées de sens. Il est préférable d'utiliser la méthode de la boucle de pointeur de base pour remplacer l'itérateur susmentionné, l'interface itérable et la boucle Forach.
Certaines opinions qui s'opposent à ce qui précède (en particulier le remplacement des itérateurs par des pointeurs) sont discutées sur Reddit pour plus de détails.
Certaines méthodes sont coûteuses. Prenant l'exemple de la branche non, nous n'avons pas mentionné les méthodes pertinentes de feuilles, mais celle-ci peut être trouvée. Supposons que notre pilote JDBC doit surmonter toutes les difficultés pour calculer la valeur de retour de la méthode de résultat.wasnull (). Le cadre SQL que nous nous implémentons peut ressembler à ceci:
if (type == Integer.class) {result = (t) wasNull (rs, Integer.valueof (Rs.GetInt (index)));} // puis ... Final statique <T> T wasNull (ResultSet rs, t Value) lance Sqlexception {return Rs.WasNull ()? null: valeur;}Dans la logique ci-dessus, la méthode de résultat.wasnull () est appelée chaque fois que la valeur int est obtenue à partir de l'ensemble de résultats, mais la méthode de getInt () est définie comme:
Type de retour: valeur variable; Si le résultat de la requête SQL est nul, retournez 0.
Ainsi, un moyen simple et efficace de l'améliorer est le suivant:
La finale statique <t étend le nombre> t wasnull (resultSet rs, t value) lève sqlexception {return (value == null || (value.intvalue () == 0 && rs.wasnull ()))? null: valeur;}C'est un jeu d'enfant.
La méthode de cache appelle pour remplacer les méthodes de frais généraux élevées sur les nœuds de feuilles ou éviter d'appeler des méthodes de frais généraux élevées si la convention de méthode le permet.
Ce qui précède introduit l'utilisation d'un grand nombre de génériques dans l'exemple de JOOQ, entraînant l'utilisation des classes d'octets, courtes, int et longues. Mais au moins les génériques ne devraient pas être une limitation de code avant qu'ils ne soient spécialisés dans les projets Java 10 ou Valhalla. Car il peut être remplacé par la méthode suivante:
// stocké sur le tas entier i = 817598;
... si vous écrivez de cette façon:
// Stockez sur la pile int i = 817598;
Les choses peuvent empirer lors de l'utilisation des tableaux:
// Trois objets ont été générés sur le tas entier [] i = {1337, 424242};... si vous écrivez de cette façon:
// Un seul objet est généré sur le tas int [] i = {1337, 424242};Lorsque nous sommes profondément dans la branche Nope, nous devons faire de notre mieux pour éviter d'utiliser des cours d'emballage. L'inconvénient de le faire est qu'il met beaucoup de pression sur GC. GC sera très occupé pour effacer les objets générés par la classe d'emballage.
Ainsi, une méthode d'optimisation efficace consiste à utiliser des types de données de base, des tableaux de longueur fixe et d'utiliser une série de variables divisées pour identifier la position de l'objet dans le tableau.
Trove4j, qui suit le protocole LGPL, est une bibliothèque de classe Java Collection, qui nous fournit une meilleure implémentation de performances que le tableau de mise en forme int [].
L'exception suivante concerne cette règle: car les types booléens et d'octets ne suffisent pas pour permettre à JDK de fournir une méthode de cache pour cela. Nous pouvons écrire de cette façon:
Booléen a1 = true; // ... Syntaxe Sugar pour: boolean a2 = boolean.valueof (true); octet b1 = (octet) 123; // ... Syntaxe Sugar pour: Byte B2 = BYTE.VALUEOF ((BYTE) 123);
D'autres types de base d'entiers ont également des situations similaires, telles que Char, Short, INT et Long.
Ne boxez pas automatiquement ces types primitifs entiers lorsque vous appelez des constructeurs ou appelez la méthode thetype.valueof ().
N'appelez pas non plus les constructeurs sur les classes de wrapper, sauf si vous souhaitez obtenir une instance qui n'est pas créée sur le tas. L'avantage de le faire est de vous donner une énorme blague de la journée des imbéciles d'avril à vos collègues.
Bien sûr, si vous souhaitez découvrir la bibliothèque de fonctions hors trépidation, bien que cela puisse être mélangé à de nombreuses décisions stratégiques, plutôt qu'à la solution locale la plus optimiste. Pour un article intéressant sur le stockage non taillé écrit par Peter Lawrey et Ben Cotton, veuillez cliquer: OpenJDK et Hashmap - Laissez les vétérans maîtriser en toute sécurité (Storage non taillé!) De nouvelles techniques.
De nos jours, les langages de programmation fonctionnelle comme Scala encouragent la récursivité. Parce que la récursivité signifie généralement la réduction de la queue qui peut être décomposée en individus optimisés individuellement. Il serait préférable que le langage de programmation que vous utilisez puisse le prendre en charge. Mais même ainsi, soyez prudent que le léger ajustement de l'algorithme transformera la récursivité de la queue en récursivité ordinaire.
Espérons que le compilateur puisse détecter cela automatiquement, sinon nous aurions gaspillé beaucoup de cadres de pile pour des choses qui peuvent être faites avec quelques variables locales.
Il n'y a rien à dire dans cette section, sauf en itérant autant que possible dans la branche non au lieu de la récursivité.
Lorsque nous voulons itérer une carte enregistrée dans les paires de valeurs clés, nous devons trouver une bonne raison du code suivant:
pour (k key: map.KeySet ()) {Vale V: map.get (key);}Sans parler de la méthode d'écriture suivante:
pour (entrée <k, v> entrée: map.entryset ()) {k key = entry.getKey (); v valeur = entrée.getValue ();}Lorsque nous utilisons la branche Nope, nous devons utiliser la carte avec prudence. Parce que de nombreuses opérations d'accès qui semblent avoir une complexité temporelle d'O (1) sont en fait composées d'une série d'opérations. Et l'accès lui-même n'est pas gratuit. Au moins, si vous devez utiliser MAP, vous devez utiliser la méthode d'entrée () pour itérer! De cette façon, nous voulons uniquement accéder à une instance de map.entry.
Lorsqu'il est nécessaire d'itérer sur la carte sous la forme de paires de valeurs clés, assurez-vous d'utiliser la méthode d'entrée ().
8. Utilisez Enumset ou Enummap
Dans certains cas, comme lorsque vous utilisez une carte de configuration, nous pouvons savoir à l'avance la valeur clé enregistrée dans la carte. Si cette valeur de clé est très petite, nous devrions envisager d'utiliser enumset ou enummap au lieu d'utiliser le hashset ou le hashmap que nous utilisons couramment. Le code suivant donne une explication claire:
Objet transitoire privé [] Vals; public v put (k key, v valeur) {// ... int index = key.ORDinal (); vals [index] = maskNull (valeur); // ...}L'implémentation clé du code précédent est que nous utilisons des tableaux au lieu des tables de hachage. Surtout lors de l'insertion de nouvelles valeurs dans la carte, tout ce que vous avez à faire est d'obtenir un numéro de séquence constant généré par le compilateur pour chaque type d'énumération. S'il existe une configuration de carte globale (par exemple, une seule instance), Enummap obtiendra des performances plus exceptionnelles que HashMap sous la pression d'une vitesse d'accès croissante. La raison en est que Enuummap utilise un peu moins la mémoire de tas que HashMap, et HashMap appelle la méthode HashCode () et la méthode equals () sur chaque valeur de clé.
Enum et Enummap sont des amis proches. Lorsque nous utilisons des valeurs clés similaires aux structures de type énumération, nous devons envisager de déclarer ces valeurs de clés comme types d'énumération et de les utiliser comme touches Enumap.
Dans le cas où Entummap ne peut pas être utilisé, au moins les méthodes HashCode () et Equals () doivent être optimisées. Une bonne méthode HashCode () est nécessaire car elle empêche les appels inutiles vers la méthode equals () à haut niveau ().
Dans la structure d'héritage de chaque classe, des objets simples facilement acceptables sont nécessaires. Jetons un coup d'œil à la façon dont Org.Jooq.Table de JOOQ est implémenté?
La méthode d'implémentation la plus simple et la plus rapide () est la suivante:
// AbstractTable une implémentation de base d'une table générale: @OverRidePublic int hashcode () {// [# 1938] par rapport aux questionnaires standard, il s'agit d'une implémentation HashCode () plus efficace. Retour nom.hashcode ();}Le nom est le nom de la table. Nous n'avons même pas besoin de considérer le schéma ou d'autres attributs de table, car les noms de table sont généralement uniques dans la base de données. Et le nom de variable est une chaîne, qui a elle-même en cache une valeur hashcode ().
Les commentaires de ce code sont très importants car abstraitable hérité de l'abstractQueryPart est la mise en œuvre de base de tout élément d'arborescence de syntaxe abstraite. Les éléments de syntaxe abstraits ordinaires n'ont pas d'attributs, ils ne peuvent donc pas avoir de fantasme sur l'optimisation de l'implémentation de la méthode HashCode (). La méthode HashCode () écrasée est la suivante:
// AbstractQueryPart A General Abstract Syntax Tree Basic Implementation: @OverRidePublic int hashcode () {// Il s'agit d'une implémentation par défaut fonctionnelle. // La sous-classe d'implémentation spécifique devrait remplacer cette méthode pour améliorer les performances. return Create (). RendeRinlined (this) .hashCode ();}En d'autres termes, l'ensemble du flux de travail de rendu SQL est déclenché pour calculer le code de hachage pour un élément d'arbre de syntaxe abstrait normal.
La méthode equals () est encore plus intéressante:
// Implémentation de base de la table générale abstractable: @OverRidePublic Boolean est égal (objet That) {if (this == that) {return true;} // [# 2144] avant d'appeler le haut-parleur à haut abstractQuerypart.equals (), // Vous pouvez savoir tôt si les objets ne sont pas égaux. if (this instanceof abstractTable) {if (stringUtils.equals (name, (((abstractTable <?>)Tout d'abord, n'utilisez pas la méthode equals () trop tôt (pas seulement en non), si:
Remarque: Si nous utilisons l'instance trop tôt pour vérifier les types compatibles, les conditions suivantes incluent en fait l'argument == null. Je l'ai déjà expliqué dans mon blog précédent, veuillez vous référer à 10 meilleures pratiques de codage Java exquises.
Une fois notre comparaison des situations ci-dessus terminées, nous devrions être en mesure de tirer quelques conclusions. Par exemple, la méthode table.equals () de JOOQ est utilisée pour comparer si les deux tables sont les mêmes. Quel que soit le type de mise en œuvre spécifique, ils doivent avoir le même nom de champ. Par exemple, les deux éléments suivants ne peuvent pas être les mêmes:
Si nous pouvons facilement déterminer si le paramètre entrant est égal à l'instance elle-même (ceci), nous pouvons abandonner l'opération si le résultat est faux. Si le résultat de retour est vrai, nous pouvons juger davantage la super mise en œuvre de la classe parent. Dans le cas où la plupart des objets comparés ne sont pas égaux, nous pouvons mettre fin à la méthode le plus tôt possible pour enregistrer le temps d'exécution du processeur.
Certains objets ont une similitude plus élevée que d'autres.
Dans JOOQ, la plupart des instances de table sont générées par le générateur de code de JOOQ, et la méthode equals () de ces instances a été profondément optimisée. Des dizaines d'autres types de tableaux (tableaux dérivés), des fonctions à valeur de table, des tables de tableau, des tables jointes, des tables de pivot, des expressions de table communes, etc., maintiennent l'implémentation de base de la méthode equals ().
Enfin, il y a une autre situation qui peut être appliquée à toutes les langues et pas seulement à Java. De plus, la branche NOPE que nous avons précédemment étudiée sera également utile pour comprendre de O (N3) à O (n log n).
Malheureusement, de nombreux programmeurs utilisent des algorithmes simples et locaux pour considérer le problème. Ils sont habitués à résoudre les problèmes étape par étape. Ceci est la forme "oui / ou" impérative. Ce style de programmation est facile à modéliser une "image plus grande" lors de la conversion de la programmation pure impérative en programmation orientée objet en programmation fonctionnelle, mais ces styles manquent de ce qui existe uniquement en SQL et R:
Programmation déclarative.
Dans SQL, nous pouvons déclarer l'effet requis à la base de données sans considérer l'influence de l'algorithme. La base de données peut adopter le meilleur algorithme selon le type de données, tels que les contraintes, les clés, les index, etc.
En théorie, nous avons d'abord eu des idées de base après SQL et des calculs relationnels. Dans la pratique, les fournisseurs SQL ont mis en œuvre des optimisateurs efficaces basés sur les frais généraux (optimisateurs basés sur les coûts) au cours des dernières décennies. Puis dans la version 2010, nous avons finalement découvert tout le potentiel de SQL.
Mais nous n'avons pas besoin d'implémenter SQL en utilisant la méthode SET. Toutes les langues et bibliothèques prennent en charge les ensembles, collections, sacs et listes. Le principal avantage de l'utilisation de l'ensemble est qu'il peut rendre notre code concis et clair. Par exemple, la méthode d'écriture suivante:
Someet se croit certains autres
Plutôt
// la méthode d'écriture précédente de Java 8 set result = new HashSet (); pour (candidat d'objet: somet) if (someotherset.contains (candidat)) result.add (candidat); // même si java 8 est utilisé, il n'est pas très utile.someset.stream (). filter (Someotherset :: contient) .Collect (collectionners.toset ());
Certaines personnes peuvent avoir des opinions différentes sur la programmation fonctionnelle et Java 8 qui peuvent nous aider à écrire des algorithmes plus simples et plus simples. Mais ce point de vue n'est pas nécessairement vrai. Nous pouvons convertir la boucle impérative Java 7 en collection de flux de Java 8, mais nous utilisons toujours le même algorithme. Mais les expressions de style SQL sont différentes:
Someet se croit certains autresLe code ci-dessus peut avoir 1000 implémentations différentes sur différents moteurs. Ce que nous étudions aujourd'hui, c'est de convertir automatiquement deux ensembles en énum avantage avant d'appeler l'opération intersecte. Même nous pouvons effectuer des opérations d'intersection parallèles sans appeler la méthode Stream.Parallel () sous-jacente.
Dans cet article, nous discutons des optimisations sur la ramification non. Par exemple, profondément dans les algorithmes à haute complexité. En tant que développeurs de JOOQ, nous sommes heureux d'optimiser la génération SQL.
JOOQ est au "bas de la chaîne alimentaire" car c'est la dernière API appelée par notre programme informatique lors de la sortie du JVM et de l'entrée du SGBD. Situé au bas de la chaîne alimentaire signifie que toute ligne prend du temps n x o x p lorsqu'elle est exécutée dans JOOQ, donc je veux l'optimiser dès que possible.
Notre logique commerciale n'est peut-être pas aussi compliquée que la branche non. Cependant, le cadre de base peut être très complexe (cadre SQL local, bibliothèques locales, etc.). Par conséquent, nous devons suivre les principes mentionnés aujourd'hui, utiliser Java Mission Control ou d'autres outils pour vérifier s'il existe un domaine qui nécessite une optimisation.
Lien original: Jaxenter Traduction: importnew.com - Toujours sur le lien de traduction routière: http://www.importnew.com/16181.html