Comment nous avons optimisé les scripts dans Unity

Il existe de nombreux articles et didacticiels sur les performances d'Unity. Nous n'essayons pas de les remplacer ou de les améliorer avec cet article, ce n'est qu'un bref résumé des étapes que nous avons suivies après avoir lu ces articles, ainsi que les étapes qui nous ont permis de résoudre nos problèmes. Je vous recommande fortement d'étudier au moins les documents sur https://learn.unity.com/ .

Dans le processus de développement de notre jeu, nous avons rencontré des problèmes qui, de temps en temps, ont provoqué une inhibition du processus de jeu. Après avoir passé du temps dans Unity Profiler, nous avons trouvé deux types de problèmes:

  • Shaders non optimisés
  • Scripts non optimisés en C #

La plupart des problèmes ont été causés par le deuxième groupe, j'ai donc décidé de me concentrer sur les scripts C # dans cet article (peut-être aussi parce que je n'ai pas écrit un seul shader dans ma vie).

Recherche de faiblesses


Le but de cet article n'est pas d'écrire un didacticiel sur l'utilisation d'un profileur; Je voulais juste parler de ce qui nous intéressait principalement pendant le processus de profilage.

Unity Profiler est toujours le meilleur moyen de trouver les causes des retards dans les scripts. Je recommande fortement de profiler le jeu directement dans l'appareil , et non dans l'éditeur. Depuis que notre jeu a été créé pour iOS, je devais connecter l'appareil et utiliser les paramètres de construction affichés dans l'image, après quoi le profileur se connectait automatiquement.


Paramètres de génération pour le profilage

Si vous essayez de google "Random lag in Unity" ou une autre demande similaire, vous constaterez que la plupart des gens recommandent de se concentrer sur la collecte des ordures , ce qui est exactement ce que j'ai fait. Des ordures sont générées chaque fois que vous cessez d'utiliser un objet (instance de classe), après quoi le garbage collector Unity démarre de temps en temps pour nettoyer le désordre et libérer de la mémoire, ce qui prend un temps fou et entraîne une baisse de la fréquence d'images.

Comment trouver des scripts indésirables dans le profileur?


Sélectionnez simplement Utilisation du processeur -> Choisir la vue Hiérarchie -> Trier par l'allocation GC


Options du profileur pour la récupération de place

Votre tâche consiste à obtenir des zéros dans la colonne d'allocation GC pour la scène de gameplay.

Un autre bon moyen est de trier les entrées par Time ms (runtime) et d'optimiser les scripts afin qu'ils prennent le moins de temps possible. Cette étape a eu un impact énorme pour nous, car l'un de nos composants contenait une grande boucle for , qui a pris une éternité (oui, nous n'avons pas encore trouvé de moyen de se débarrasser de la boucle), donc l'optimisation du temps d'exécution de tous les scripts était absolument nécessaire pour nous, car nous devions économiser le temps d'exécution sur cette boucle coûteuse, tout en maintenant une fréquence stable de 60 ips.

Sur la base des données de profilage, j'ai divisé l'optimisation en deux parties:

  • Élimination des ordures
  • Délai d'exécution réduit

Partie 1: combattre les déchets


Dans cette partie, je vais vous dire ce que nous avons fait pour nous débarrasser des ordures. Il s'agit des connaissances les plus fondamentales que tout développeur doit comprendre; ils sont devenus une partie importante de notre analyse quotidienne dans chaque demande de pull / merge.

Première règle: pas de nouveaux objets dans les méthodes de mise à jour


Idéalement, les méthodes Update, FixedUpdate et LateUpdate ne doivent pas contenir les "nouveaux" mots clés . Vous devez toujours utiliser ce que vous avez déjà.

Parfois, la création d'un nouvel objet est masquée dans certaines méthodes Unity internes, ce n'est donc pas si évident. Nous en reparlerons plus tard.

Deuxième règle: créez une fois et réutilisez!


En substance, cela signifie que vous devez allouer de la mémoire pour tout ce que vous pouvez dans les méthodes Start et Awake. Cette règle est très similaire à la première. Il s'agit en fait d'une autre façon d'éliminer les «nouveaux» mots clés des méthodes de mise à jour.

Code que:

  • crée de nouvelles instances
  • à la recherche d'objets de jeu

Vous devez toujours essayer de passer des méthodes Update à Start ou Awake.

Voici des exemples de nos changements:

Allocation de mémoire pour les listes de la méthode Start, leur effacement (Clear) et réutilisation si nécessaire.

//Bad code private List<GameObject> objectsList; void Update() { objectsList = new List<GameObject>(); objectsList.Add(......) } //Better Code private List<GameObject> objectsList; void Start() { objectsList = new List<GameObject>(); } void Update() { objectsList.Clear(); objectsList.Add(......) } 

Stocker les liens et les réutiliser comme suit:

 //Bad code void Update() { var levelObstacles = FindObjectsOfType<Obstacle>(); foreach(var obstacle in levelObstacles) { ....... } } //Better code private Object[] levelObstacles; void Start() { levelObstacles = FindObjectsOfType<Obstacle>(); } void Update() { foreach(var obstacle in levelObstacles) { ....... } } 

La même chose s'applique à la méthode FindGameObjectsWithTag ou à toute autre méthode qui renvoie un nouveau tableau.

La troisième règle: méfiez-vous des chaînes et évitez de les enchaîner


Quand il s'agit de créer des ordures, les lignes sont terribles. Même les opérations de chaîne les plus simples peuvent créer beaucoup de déchets. Pourquoi? Les chaînes ne sont que des tableaux et ces tableaux sont immuables. Cela signifie que chaque fois que vous concaténez deux lignes, un nouveau tableau est créé et l'ancien se transforme en ordures. Heureusement, StringBuilder peut être utilisé pour éviter ou minimiser une telle création de déchets.

Voici un exemple de la façon dont vous pouvez améliorer la situation:

 //Bad code void Start() { text = GetComponent<Text>(); } void Update() { text.text = "Player " + name + " has score " + score.toString(); } //Better code void Start() { text = GetComponent<Text>(); builder = new StringBuilder(50); } void Update() { //StringBuilder has overloaded Append method for all types builder.Length = 0; builder.Append("Player "); builder.Append(name); builder.Append(" has score "); builder.Append(score); text.text = builder.ToString(); } 

Tout va bien avec l'exemple ci-dessus, mais il existe encore de nombreuses possibilités pour améliorer le code. Comme vous pouvez le voir, presque toute la chaîne peut être considérée comme statique. Nous avons divisé la chaîne en deux parties pour deux objets UI.Text. Premièrement, l'un ne contient que le texte statique "Player" + nom + "a un score" , qui peut être attribué dans la méthode Start, et le second contient la valeur du score, qui est mise à jour dans chaque image. Faites toujours des lignes statiques vraiment statiques et générez-les dans la méthode Start ou Awake . Après cette amélioration, presque tout est en ordre, mais un peu de déchets sont toujours générés lors de l'appel de Int.ToString (), Float.ToString (), etc.

Nous avons résolu ce problème en générant et en pré-allouant de la mémoire pour toutes les lignes possibles. Cela peut sembler un stupide gaspillage de mémoire, mais une telle solution est parfaitement adaptée à nos besoins et résout complètement le problème. Donc, à la fin, nous avons obtenu un tableau statique, dont l'accès est directement accessible à l'aide d'index pour prendre la chaîne souhaitée indiquant un nombre:

 public static readonly string[] NUMBERS_THREE_DECIMAL = { "000", "001", "002", "003", "004", "005", "006",.......... 

Quatrième règle: valeurs de cache renvoyées par les méthodes d'accès


Cela peut être très difficile, car même une simple méthode d'accesseur comme celle illustrée ci-dessous génère des ordures:

 //Bad Code void Update() { gameObject.tag; //or gameObject.name; } 

Essayez d'éviter d'utiliser des méthodes d'accès dans la méthode Update. Appelez la méthode d'accès une seule fois dans la méthode Start et mettez en cache la valeur de retour.

En général, je recommande de NE PAS appeler de méthodes d'accès aux chaînes ou de méthodes d'accès aux tableaux dans la méthode Update . Dans la plupart des cas, il suffit d'obtenir le lien une fois dans la méthode Start .

Voici deux exemples plus courants d'un autre code de méthode d'accès non optimisé:

 //Bad Code void Update() { //Allocates new array containing all touches Input.touches[0]; } //Better Code void Update() { Input.GetTouch(0); } //Bad Code void Update() { //Returns new string(garbage) and compare the two strings gameObject.Tag == "MyTag"; } //Better Code void Update() { gameObject.CompareTag("MyTag"); } 

Cinquième règle: utiliser des fonctions qui n'allouent pas de mémoire


Pour certaines fonctions Unity, des alternatives sans mémoire peuvent être trouvées. Dans notre cas, toutes ces fonctions sont liées à la physique. Notre reconnaissance des collisions est basée sur

 Physics2D. CircleCast(); 

Dans ce cas particulier, vous pouvez trouver une fonction non mémoire appelée

 Physics2D. CircleCastNonAlloc(); 

De nombreuses autres fonctions ont également des alternatives similaires, vérifiez donc toujours la documentation des fonctions NonAlloc .

Sixième règle: ne pas utiliser LINQ


Ne le fais pas. Je veux dire, vous n'avez pas besoin de l'utiliser dans un code qui s'exécute fréquemment. Je sais que lors de l'utilisation de LINQ, le code est plus facile à lire, mais dans de nombreux cas, les performances et l'allocation de mémoire d'un tel code sont terribles. Bien sûr, il peut parfois être utilisé, mais, pour être honnête, dans notre jeu, nous n'utilisons pas du tout LINQ.

Septième règle: créer une fois et réutiliser, partie 2


Cette fois, nous parlons de regrouper des objets. Je n'entrerai pas dans les détails de la mutualisation, car cela a été dit plusieurs fois, par exemple, étudiez ce tutoriel: https://learn.unity.com/tutorial/object- Covoiturage

Dans notre cas, le script de regroupement d'objets suivant est utilisé. Nous avons un niveau généré rempli d'obstacles qui existent pendant une certaine période de temps jusqu'à ce que le joueur passe cette partie du niveau. Des instances de tels obstacles sont créées à partir de préfabriqués si certaines conditions sont remplies. Le code est dans la méthode Update. Ce code est complètement inefficace en termes de mémoire et d'exécution. Nous avons résolu le problème en générant un pool de 40 obstacles: si nécessaire, nous récupérons les obstacles du pool et renvoyons l'objet dans le pool lorsqu'il n'est plus nécessaire.

La huitième règle: plus attentivement avec le packaging-transformation (Boxe)!


La boxe génère des déchets! Mais qu'est-ce que la boxe? Le plus souvent, la boxe se produit lorsque vous transmettez un type de valeur (int, float, bool, etc.) à une fonction qui attend un objet de type Object.

Voici un exemple de boxe que nous devons corriger dans notre projet:

Nous avons implémenté notre propre système de messagerie dans le projet. Chaque message peut contenir une quantité illimitée de données. Les données sont stockées dans un dictionnaire défini comme suit:

 Dictionary<string, object> data; 

Nous avons également un setter qui définit les valeurs dans ce dictionnaire:

 public Action SetAttribute(string attribute, object value) { data[attribute] = value; } 

La boxe ici est assez évidente. Vous pouvez appeler la fonction comme suit:

 SetAttribute("my_int_value", 12); 

Ensuite, la valeur «12» est soumise à la boxe et cela génère des ordures.

Nous avons résolu le problème en créant des conteneurs de données distincts pour chaque type primitif, et le conteneur d'objets précédent est utilisé uniquement pour les types de référence.

 Dictionary<string, object> data; Dictionary<string, bool> dataBool; Dictionary<string, int> dataInt; ....... 

Nous avons également des setters séparés pour chaque type de données:

 SetBoolAttribute(string attribute, bool value) SetIntAttribute(string attribute, int value) 

Et tous ces setters sont implémentés de telle manière qu'ils appellent la même fonction généralisée:

 SetAttribute<T>(ref Dictionary<string, T> dict, string attribute, T value) 

Le problème de boxe a été résolu!

En savoir plus à ce sujet dans l'article https://docs.microsoft.com/cs-cz/dotnet/csharp/programming-guide/types/boxing-and-unboxing .

La neuvième règle: les cycles sont toujours suspects


Cette règle est très similaire à la première et à la seconde. Essayez simplement de supprimer tout le code facultatif des boucles pour des raisons de performances et de mémoire.

Dans le cas général, nous nous efforçons de nous débarrasser des boucles dans les méthodes de mise à jour, mais si nous ne pouvons pas nous en passer, nous éviterons au moins toute allocation de mémoire dans ces boucles. Suivez donc les règles 1 à 8 et appliquez-les aux boucles en général, pas seulement aux méthodes de mise à jour.

Règle 10: pas de déchets dans les bibliothèques externes


Dans le cas où il s'avère qu'une partie de la poubelle est générée par du code téléchargé depuis le magasin de ressources, ce problème a de nombreuses solutions. Mais avant de faire de l'ingénierie inverse et du débogage, revenez simplement au magasin de ressources et mettez à jour la bibliothèque. Dans notre cas, tous les actifs utilisés étaient toujours pris en charge par les auteurs qui ont continué à publier des mises à jour améliorant les performances, ce qui a résolu tous nos problèmes. Les dépendances doivent être pertinentes! Je préfère me débarrasser de la bibliothèque plutôt que de rester sans support.

Partie 2: maximiser l'exécution


Certaines des règles ci-dessus font une différence subtile si le code est rarement appelé. Il y a une grande boucle dans notre code qui s'exécute dans chaque image, donc même ces petits changements ont eu un effet énorme.

Certains de ces changements, s'ils sont mal utilisés ou dans une mauvaise situation, peuvent entraîner une durée d'exécution encore pire. Vérifiez toujours le profileur après avoir entré chaque optimisation dans le code pour vous assurer que vous vous déplacez dans la bonne direction .

Honnêtement, certaines de ces règles entraînent un code lisible bien pire , et parfois même une violation des recommandations , par exemple, l'incorporation de code mentionnée dans l'une des règles ci-dessous.

Bon nombre de ces règles chevauchent celles présentées dans la première partie de l'article. En règle générale, les performances du code générateur de déchets sont inférieures par rapport au code sans générateur de déchets.

La première règle: l'ordre d'exécution correct


Déplacez le code des méthodes FixedUpdate, Update, LateUpdate vers les méthodes Start et Awake . Je sais que cela semble fou, mais croyez-moi, si vous fouillez dans votre code, vous trouverez des centaines de lignes de code qui peuvent être déplacées vers des méthodes qui ne sont exécutées qu'une seule fois.

Dans notre cas, ce code est généralement associé à

  • Appels à GetComponent <>
  • Calculs qui retournent en fait le même résultat dans chaque image
  • Plusieurs instances des mêmes objets, généralement des listes
  • Rechercher GameObjects
  • Obtenir des liens vers Transform et utiliser d'autres méthodes d'accès

Voici une liste d'exemples de code que nous avons déplacés des méthodes de mise à jour vers les méthodes de démarrage:

 //There must be a good reason to keep GetComponent in Update gameObject.GetComponent<LineRenderer>(); gameObject.GetComponent<CircleCollider2D>(); //Examples of calculations returning same result every frame Mathf.FloorToInt(Screen.width / 2); var width = 2f * mainCamera.orthographicSize * mainCamera.aspect; var castRadius = circleCollider.radius * transform.lossyScale.x; var halfSize = GetComponent<SpriteRenderer>().bounds.size.x / 2f; //Finding objects var levelObstacles = FindObjectsOfType<Obstacle>(); var levelCollectibles = FindGameObjectsWithTag("COLLECTIBLE"); //References objectTransform = gameObject.transform; mainCamera = Camera.main; 

Deuxième règle: exécuter du code uniquement lorsque cela est nécessaire


Dans notre cas, cela concerne principalement les scripts de mise à jour de l'interface utilisateur. Voici un exemple de la façon dont nous avons modifié l'implémentation du code qui affiche l'état actuel des éléments collectés au niveau.

 //Bad code Text text; GameState gameState; void Start() { gameState = StoreProvider.Get<GameState>(); text = GetComponent<Text>(); } void Update() { text.text = gameState.CollectedCollectibles.ToString(); } 

Étant donné qu'à chaque niveau il n'y a que quelques éléments à collecter, cela n'a aucun sens de changer le texte de l'interface utilisateur dans chaque cadre. Par conséquent, nous modifions le texte uniquement lorsque le nombre change.

 //Better code Text text; GameState gameState; int collectiblesCount; void Start() { gameState = StoreProvider.Get<GameState>(); text = GetComponent<Text>(); collectiblesCount = gameState.CollectedCollectibles; } void Update() { if(collectiblesCount != gameState.CollectedCollectibles) { //This code is ran only about 5 times each level collectiblesCount = gameState.CollectedCollectibles; text.text = collectiblesCount.ToString(); } } 

Ce code est bien meilleur, surtout si les actions sont beaucoup plus compliquées que de simplement changer l'interface utilisateur.

Si vous recherchez une solution plus complète, je vous recommande d'implémenter le modèle Observer à l' aide d'événements C # ( https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/ ).

Quoi qu'il en soit, cela ne nous suffisait pas encore, et nous voulions implémenter une solution complètement généralisée, nous avons donc créé une bibliothèque qui implémente Flux in Unity. Cela a conduit à une solution très simple, dans laquelle tout l'état du jeu est stocké dans l'objet «Store», et tous les éléments d'interface utilisateur et autres composants sont notifiés lorsque l'état change et réagissent à ce changement sans code dans la méthode Update.

Troisième règle: les cycles sont toujours suspects


C'est exactement la même règle que j'ai mentionnée dans la première partie de l'article. S'il y a une boucle dans le code qui contourne de manière itérative un grand nombre d'éléments, pour améliorer les performances de la boucle, utilisez les deux règles des deux parties de l'article.

Quatrième règle: pour mieux que Foreach


La boucle Foreach est très facile à écrire, mais "très difficile" à exécuter. À l'intérieur de la boucle Foreach, Enumerator est utilisé pour traiter de manière itérative l'ensemble de données et renvoyer la valeur. C'est plus compliqué que d'itérer sur des indices dans une simple boucle For.

Par conséquent, dans notre projet, nous avons chaque fois que possible remplacé les boucles Foreach par For:

 //Bad code foreach (GameObject obstacle in obstacles) //Better code var count = obstacles.Count; for (int i = 0; i < count; i++) { obstacles[i]; } 

Dans notre cas avec une grande boucle for, ce changement est très important. Une simple boucle for accélère deux fois le code .

Cinquième règle: les tableaux sont meilleurs que les listes


Dans notre code, nous avons découvert que la plupart des listes sont de longueur constante, ou nous pouvons calculer le nombre maximal d'éléments. Par conséquent, nous les avons réimplémentées sur la base de tableaux, et dans certains cas, cela a conduit à une double accélération des itérations sur les données.

Dans certains cas, les listes ou autres structures de données complexes ne peuvent pas être évitées. Il arrive que vous deviez souvent ajouter ou supprimer des éléments, et dans ce cas, il est préférable d'utiliser des listes. Mais en général, les tableaux doivent toujours être utilisés pour les listes de longueur fixe .

Sixième règle: les opérations flottantes sont meilleures que les opérations vectorielles


Cette différence est à peine perceptible si vous n'effectuez pas des milliers de telles opérations, comme c'était le cas dans notre cas, donc pour nous l'augmentation de la productivité a été significative.

Nous avons apporté des modifications similaires:

 Vector3 pos1 = new Vector3(1,2,3); Vector3 pos2 = new Vector3(4,5,6); //Bad code var pos3 = pos1 + pos2; //Better code var pos3 = new Vector3(pos1.x + pos2.x, pos1.y + pos2.y, ......); Vector3 pos1 = new Vector3(1,2,3); //Bad code var pos2 = pos1 * 2f; //Better code var pos2 = new Vector3(pos1.x * 2f, pos1.y * 2f, ......); 

Septième règle: rechercher correctement les objets


Pensez toujours à savoir si vous devez vraiment utiliser la méthode GameObject.Find (). Cette méthode est lourde et prend un temps fou. Vous ne devez jamais utiliser cette méthode dans les méthodes de mise à jour. Nous avons constaté que la plupart de nos appels Find peuvent être remplacés par des liens directs dans l'éditeur , ce qui, bien sûr, est bien meilleur.

 //Bad Code GameObject player; void Start() { player = GameObject.Find("PLAYER"); } //Better Code //Assign the reference to the player object in editor [SerializeField] GameObject player; void Start() { } 

Si cela est impossible, envisagez au moins d' utiliser des balises (Tag) et de rechercher un objet par son étiquette à l'aide de GameObject.FindWithTag .

Donc, dans le cas général: lien direct> GameObject.FindWithTag ()> GameObject.Find ()

Huitième règle: travailler uniquement avec des objets pertinents


Dans notre cas, cela était important pour reconnaître les collisions à l'aide de RayCast-s (CircleCast, etc.). Au lieu de reconnaître les collisions et de décider lesquelles sont importantes dans le code, nous avons déplacé les objets du jeu vers les couches appropriées afin de pouvoir calculer les collisions uniquement pour les objets nécessaires.

Voici un exemple

 //Bad Code void DetectCollision() { var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance); for (int i = 0; i < count; i++) { var obj = results[i].collider.transform.gameObject; if(obj.CompareTag("FOO")) { ProcessCollision(results[i]); } } } //Better Code //We added all objects with tag FOO into the same layer void DetectCollision() { //8 is number of the desired layer var mask = 1 << 8; var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance, mask); for (int i = 0; i < count; i++) { ProcessCollision(results[i]); } } 

La neuvième règle: utiliser correctement les étiquettes


Il ne fait aucun doute que les étiquettes sont très utiles et peuvent améliorer les performances du code, mais n'oubliez pas qu'il n'y a qu'une seule façon correcte de comparer les étiquettes d'objets !

 //Bad Code gameObject.Tag == "MyTag"; //Better Code gameObject.CompareTag("MyTag"); 

La dixième règle: méfiez-vous des astuces avec l'appareil photo!


Il est si facile d'utiliser Camera.main , mais les performances de cette action sont très médiocres. La raison en est que dans les coulisses de chaque appel à Camera.main, le moteur Unity exécute en fait le résultat FindGameObjectsWithTag (), donc nous comprenons déjà que vous n'avez pas besoin de l'appeler souvent, et il est préférable de résoudre ce problème en mettant en cache le lien dans la méthode Start ou éveillé.

 //Bad code void Update() { Camera.main.orthographicSize //Some operation with camera } //Better Code private Camera cam; void Start() { cam = Camera.main; } void Update() { cam.orthographicSize //Some operation with camera } 

Onzième règle: LocalPosition vaut mieux que Position


Dans la mesure du possible, utilisez Transform.LocalPosition pour les getters et les setters au lieu de Transform.Position . Dans chaque appel Transform.Position, beaucoup plus d'opérations sont effectuées, par exemple, le calcul de la position globale dans le cas d'un appel getter ou le calcul de la position locale à partir du global dans le cas d'un appel setter. Dans notre projet, il s'est avéré que vous pouvez utiliser LocalPositions dans 99% des cas en utilisant Transform.Position, et vous n'avez pas besoin d'apporter d'autres modifications dans le code.

Douzième règle: n'utilisez pas LINQ


Cela a déjà été discuté dans la première partie. Ne l'utilisez pas, c'est tout.

Treizième règle: n'ayez pas peur (parfois) d'enfreindre les règles


Parfois, même appeler une fonction simple peut être trop coûteux. Dans ce cas, vous devez toujours envisager d'incorporer du code (Code Inlining). Qu'est-ce que cela signifie? En fait, nous prenons simplement le code de la fonction et le copions directement à l'endroit où nous voulons utiliser la fonction pour éviter d'appeler des méthodes supplémentaires.

Dans la plupart des cas, cela n'aura aucun effet, car l'incorporation du code est effectuée automatiquement au stade de la compilation, mais il existe certaines règles selon lesquelles le compilateur décide d'incorporer le code (par exemple, les méthodes virtuelles ne sont jamais intégrées; pour plus de détails, voir https: //docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html ). Il suffit donc d'ouvrir le profileur, de lancer le jeu sur l'appareil cible et de voir si quelque chose peut être amélioré.

Dans notre cas, il y avait plusieurs fonctions que nous avons décidé d'intégrer pour améliorer les performances, en particulier dans la grande boucle for.

Conclusion


En appliquant les règles énumérées dans l'article, nous avons facilement atteint 60 fps stables dans le jeu pour iOS, même sur l'iPhone 5S. Peut-être que certaines des règles peuvent être spécifiques à notre projet, mais je pense que la plupart d'entre elles doivent être mémorisées lors de l'écriture du code ou de sa vérification afin d'éviter des problèmes à l'avenir. Il est toujours préférable d'écrire constamment du code en fonction des performances que plus tard pour refactoriser de gros morceaux de code.

Source: https://habr.com/ru/post/fr481980/


All Articles