Classes scellées. Sémantique vs performance

Probablement pas moi seul après avoir lu la documentation sur les cours scellés, j'ai pensé: «D'accord. Peut-être que cela vous sera utile un jour. » Plus tard, lorsque dans mon travail j'ai rencontré quelques tâches où j'ai réussi à utiliser cet outil avec succès, j'ai pensé: «Ce n'est pas mal. Vous devriez souvent penser à l'application. " Et enfin, je suis tombé sur une description de la classe de tâches dans le livre Effective Java (Joshua Bloch, 3e) (oui, dans le livre sur Java).

Examinons une application et évaluons-la en termes de sémantique et de performances.


Je pense que tous ceux qui ont travaillé avec l'interface utilisateur ont rencontré une fois la mise en œuvre de l'interaction de l'interface utilisateur avec le service à travers certains États , où l'un des attributs était un marqueur de type . La mécanique du traitement de l'état suivant dans de telles implémentations dépend généralement directement du marqueur spécifié. Par exemple, une telle implémentation de la classe State :

class State( val type: Type, val data: String?, val error: Throwable? ) { enum class Type { LOADING, ERROR, EMPTY, DATA } } 

Nous listons les inconvénients d'une telle implémentation (essayez-la vous-même)
Remarques du chapitre 23 du livre «Préférer les hiérarchies de classes aux classes balisées». Je propose de la connaître.

  1. Consommation de mémoire pour les attributs initialisés uniquement pour certains types. Le facteur peut être important dans les grands volumes. La situation est exacerbée si des objets par défaut sont créés pour remplir les attributs.
  2. Charge sémantique excessive. L'utilisateur de la classe doit surveiller quel type, quels attributs sont disponibles.
  3. Prise en charge compliquée dans les classes de logique métier. Supposons une implémentation où un objet peut effectuer certaines opérations sur ses données. Une telle classe ressemblera à une moissonneuse , et l'ajout d'un nouveau type ou d'une nouvelle opération peut devenir difficile.


Le traitement d'un nouvel état peut ressembler à ceci:

 fun handleState(state: State) { when(state.type) { State.Type.LOADING -> onLoading() State.Type.ERROR -> state.error?.run(::onError) ?: throw AssertionError("Unexpected error state: $state") State.Type.EMPTY -> onEmpty() State.Type.DATA -> state.data?.run(::onData) ?: throw AssertionError("Unexpected data state: $state") } } fun onLoading() {} fun onError(error: Throwable) {} fun onEmpty() {} fun onData(data: String) {} 

Veuillez noter que pour des états tels que ERREUR et DONNÉES, le compilateur n'est pas en mesure de déterminer la sécurité de l'utilisation des attributs, donc l'utilisateur doit écrire du code redondant. Les changements de sémantique ne peuvent être détectés qu'au moment de l'exécution.

Classe scellée


image

Avec une simple refactorisation, nous pouvons diviser notre État en un groupe de classes:

 sealed class State //    stateless  -     singleton object Loading : State() data class Error(val error: Throwable) : State() //  ,     ,  stateless  -  singleton object Empty : State() data class Data(val data: String) : State() 

Du côté de l'utilisateur, nous obtenons un traitement d'état, où l'accessibilité des attributs sera déterminée au niveau de la langue, et une mauvaise utilisation entraînera des erreurs au stade de la compilation:

 fun handleState(state: State) { when(state) { Loading -> onLoading() is Error -> onError(state.error) Empty -> onEmpty() is Data -> onData(state.data) } } 

Étant donné que seuls les attributs significatifs sont présents dans les copies, nous pouvons parler d'économiser de la mémoire et, surtout, d'améliorer la sémantique. Les utilisateurs de classes scellées n'ont pas besoin d'implémenter manuellement les règles de travail avec les attributs en fonction du marqueur de type ; la disponibilité des attributs est assurée par la séparation en types.

Tout cela est-il gratuit?


Spoiler
Non, pas gratuitement.

Les développeurs Java qui ont essayé Kotlin doivent avoir regardé le code décompilé pour voir à quoi ressemblent les termes Java de Kotlin. Une expression avec quand ressemblera à ceci:

 public static final void handleState(@NotNull State state) { Intrinsics.checkParameterIsNotNull(state, "state"); if (Intrinsics.areEqual(state, Loading.INSTANCE)) { onLoading(); } else if (state instanceof Error) { onError(((Error)state).getError()); } else if (Intrinsics.areEqual(state, Empty.INSTANCE)) { onEmpty(); } else if (state instanceof Data) { onData(((Data)state).getData()); } } 

Les branches avec une abondance d' instanceof peuvent être alarmantes à cause des stéréotypes sur le «signe de mauvais code» et «l'impact sur les performances», mais nous n'en avons aucune idée. Il est nécessaire de comparer en quelque sorte la vitesse d'exécution, par exemple en utilisant jmh .

Sur la base de l'article «Mesurer correctement la vitesse du code Java» , un test de traitement de quatre états (CHARGEMENT, ERREUR, VIDE, DONNÉES) a été préparé, voici ses résultats:

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s 

On peut voir que la mise en œuvre scellée fonctionne ≈25% plus lentement (on supposait que le décalage ne dépasserait pas 10-15%).

Si sur quatre types nous avons un quart de décalage, avec des types croissants (le nombre d'instances de contrôles), le décalage ne devrait que croître. Pour vérifier, nous allons augmenter le nombre de types à 16 (supposons que nous avons réussi à obtenir une hiérarchie aussi large):

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s 

Parallèlement à une baisse de la productivité, le décalage des ventes scellées est passé à ≈35% - un miracle ne s'est pas produit.

Conclusion


Dans cet article, nous n'avons pas découvert l'Amérique et les implémentations scellées dans les branches sur instanceof fonctionnent vraiment plus lentement que les comparaisons de liens.


Néanmoins, quelques réflexions doivent être exprimées:

  • dans la plupart des cas, nous voulons travailler avec une bonne sémantique de code, accélérer le développement grâce à des conseils supplémentaires de l'IDE et des vérifications du compilateur - dans de tels cas, vous pouvez utiliser des classes scellées
  • si vous ne pouvez pas sacrifier les performances d'une tâche, vous devez négliger l'implémentation scellée et la remplacer, par exemple, par une implémentation de tagget. Vous devriez peut-être abandonner complètement la kotlin au profit des langues de niveau inférieur

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


All Articles