Introduction complet au Framework JAVA BioJava
Introduction complet au Framework JAVA BioJava
BioJava est destiné à fournir des API Java pour les tâches bioinformatiques courantes. Il s’efforce également d’être une base pratique pour écrire des algorithmes potentiellement coûteux en calcul. Pour réduire la courbe d'apprentissage et réduire les coûts de maintenance, les API individuelles doivent être suffisamment complètes pour pouvoir être utilisées de manière algorithmique, mais suffisamment minces pour pouvoir être facilement implémentées et utilisées. L’équilibre entre rendre les API non seulement puissantes mais aussi petites est parfois difficile à maintenir, mais a, à mon avis, favorisé un degré élevé d’élégance dans la conception de l’objet sous-jacent.
Il existe deux cas clairement différents dans lesquels la réutilisation de code est bénéfique. Le premier cas, et le plus couramment envisagé, est la réutilisation du code de bibliothèque en l'invoquant à partir de plusieurs applications. Par exemple, il est très courant de réutiliser une bibliothèque de mathématiques matricielles lors de la programmation numérique. Nous pourrions appeler cela le cas «new using old». L'autre cas de réutilisation concerne le moment où le code de bibliothèque peut exécuter une procédure éprouvée qui appelle à son tour un code spécifique à une application. Par exemple, en Java, un écouteur peut être enregistré dans une fenêtre pour gérer les événements de déplacement de la souris. Dans ce cas, le code de la bibliothèque est chargé de dessiner la fenêtre et d’appeler l’auditeur des mouvements de la souris, mais le comportement exact de la bibliothèque est personnalisé par l’auditeur. Nous pourrions appeler cela le cas «vieux avec nouveau».
La conception et la mise en œuvre des bibliothèques BioJava ont été principalement un exercice informatique, pas de biologie. Tout au long de notre travail, nous nous sommes efforcés d’encourager un degré élevé de réutilisation du code, à la fois en fournissant des API utilisables dans un large éventail de contextes (nouveaux et anciens), et en offrant aux développeurs l’occasion de proposer de nouvelles implémentations sans affecter les environnements existants. code (ancien utilisant nouveau).
Les API pertinentes pour cette thèse, et pour lesquelles j'ai été concepteur principal et implémenteur, sont les suivantes:
- Exceptions et assertions imbriquées
- possibilité de changement
- Symboles, alphabets et liste de symboles
- Sequence, Feature et SequenceDB
- Distribution
- MarkovModel
- Requête
Dans la mesure du possible, l'API est définie en termes de définitions d'interface Java, permettant l'intégration transparente de plusieurs implémentations. En effet, le recours à des interfaces a rendu le développement de la bibliothèque BioJava relativement rapide et robuste. Nous avons découvert que presque tous les types de données de base peuvent être implémentés de plusieurs façons en fonction de nombreux facteurs. L'ensemble de la boîte à outils n'émet donc que peu d'hypothèses sur la mise en œuvre.
2.1 Java comme langage pour la bioinformatique
Java (Gosling, Joy et al. 2000) est un langage créé par Sun Microsystems, initialement conçu pour être utilisé dans des systèmes intégrés tels que les téléphones mobiles, les montres et les systèmes ABS. Il s'appuie sur la définition d'une machine virtuelle (VM) (Lindholm et Yellin, 1999) responsable de l'allocation des threads et de la mémoire, de l'exécution des instructions en code octet et de l'application des restrictions de sécurité. Le code d'octet est naturellement orienté objectivement et prend en charge des fonctionnalités avancées telles que la gestion des exceptions et la synchronisation des threads. Le code d'octet agit sur une pile de variables de travail, sur un ensemble arbitrairement volumineux de registres virtuels et sur l'objet (ou la classe) actuellement dans la portée. Il n'y a pas de type de pointeur en Java, ni dans le code d'octet, ce qui rend impossible l'écriture de code d'octet adressant de la mémoire arbitraire. Les prouveurs de théorèmes peuvent être utilisés pour valider qu’une partie donnée du code octet peut être exécutée en toute sécurité, évitant ainsi certains problèmes liés à d’autres langages (tels que l’allocation de mémoire non valide, l’exécution d’instructions sur des types inappropriés et la validation que la pile d’exécution est toujours dans une configuration incorrecte). état cohérent).
La machine virtuelle Java est chargée de l’interprétation du code octet et des fonctions d’environnement telles que l’allocation de mémoire et le garbage collection (libération des objets du pool de mémoire une fois qu’elles ne sont plus dans la portée), d’appels système (E / S, exécution de processus, gestion des threads) et gestion des homologues de l’interface utilisateur graphique du système d’exploitation natif. Avec une machine virtuelle d’une version donnée (par exemple 1.2.2) et n’importe quelle plate-forme (par exemple Sun pour Windows, Compaq pour Tru64), l’exécution d’une partie du code octet devrait produire exactement les mêmes résultats, même si les performances diffèrent. Cette portabilité du code par conception est historiquement l’un des principaux avantages de Java. En pratique, les incompatibilités de plate-forme résultent presque toujours de parties de la machine virtuelle spécifiques à la plate-forme, telles que des homologues graphiques, plutôt que des bogues dans l'exécution du code octet.
L'interprétation de code par octet Java pur a toujours été lente par rapport au code natif (compilé à partir de C / C ++ ou de FORTRAN, par exemple), mais a toujours été comparée favorablement à d'autres langages interprétés tels que Perl. Récemment, avec le passage de Java des exemples de jouets et des petites applications graphiques aux grandes applications exigeantes telles que les serveurs dorsaux de serveur Web (notamment J2EE) et le traitement numérique à grande échelle (par exemple, la bibliothèque de mathématiques Colt matrix), les technologies ont semblé améliorer les performances.
Initialement, les compilateurs juste-à-temps (JIT) augmentaient les performances de gros blocs de code numérique comparables à ceux du C ++ en compilant chaque fonction de code octet en code natif pour le processeur physique une fois qu'une classe avait été chargée (présent même dans de nombreux programmes Java1). .1 ordinateurs virtuels). Certaines instructions de code octet Java peuvent être représentées proprement comme une ou plusieurs instructions natives simples (par exemple, les opérations arithmétiques). Cependant, de nombreuses instructions de code octet Java n'ayant pas de représentation directe (telles que l'allocation d'objet ou l'appel de méthode), elles doivent donc être converties en appels à la machine virtuelle. Les compilateurs JIT ont tendance à augmenter les performances de code numérique ressemblant plus à des styles de programmation procéduraux classiques. Les JIT se sont révélés insuffisants pour de nombreuses tâches car de nombreuses méthodes Java sont petites et sont souvent exécutées en tant qu'appels virtuels qui ne peuvent pas être résolus au moment de la compilation. De plus, en raison de la nature hautement polymorphe de la plupart des codes Java, il est souvent impossible de procéder à des optimisations car le système à base de types simple ne peut pas donner suffisamment d’informations pour connaître le contexte dans lequel le code sera exécuté.
La dernière famille de machines virtuelles est basée sur l’architecture de la machine virtuelle Hotspot de Sun. Cela fait appel à plusieurs techniques pour supprimer les goulots d'étranglement liés aux performances et optimiser l'exécution du code. Tout d'abord, une grande partie du temps d'exécution de Java peut être utilisée pour l'allocation d'objets et le garbage collection. Cela est particulièrement coûteux pour les objets alloués puis abandonnés dans les boucles internes. Hotspot marque initialement les objets avec une heure de création et les place dans une zone de garderie. Lorsque plus de mémoire est nécessaire, Hotspot tente d’abord de libérer des objets dans la pépinière plutôt que de terminer un cycle complet de collecte des déchets.
Comme exemple anecdotique de l’impact que cela peut avoir sur les performances, j’ai écrit un code qui allouait inutilement un grand nombre d’objets dans une boucle serrée, puis j’ai exécuté l’application sur un PIII 800 MHz avec la machine virtuelle Hotspot et un Compaq DS40 avec Compaq 1.2. 2 VM rapide. Après un jour et demi, le processus sur le DS40 n'avait toujours pas abouti. Sur le PC, cela s'est terminé après 110 secondes. Une fois que les objets inutiles n’étaient plus créés, le serveur Compaq n’exécutait que 20 secondes et le PC 56 secondes. Cela indique clairement l’impact de l’implémentation de la VM sur les performances.
Après une gestion minutieuse de la mémoire, le deuxième principe d'optimisation du code est que 5% du code comptera pour 95% du temps d'exécution. Si vous souhaitez concentrer vos efforts sur l'optimisation, cette partie est celle à cibler. La machine virtuelle hotspot profile en permanence l'application et optimise simultanément chacun des points chauds d'exécution. Cela se traduit par une augmentation des performances des applications au fur et à mesure de leur exécution (souvent par des facteurs supérieurs à 10 fois).
Les invocations de méthodes rendent de nombreuses optimisations impossibles, en particulier si la méthode est liée au moment de l'exécution plutôt qu'à la compilation (par exemple, les appels de méthodes virtuelles). En effet, l’optimiseur ne sait pas quels seront les effets secondaires du code invoqué. Il ne peut donc pas effectuer d’optimisations agressives pour éliminer le code redondant ou pour réorganiser les instructions lors des appels de fonction.
Dans les langages traditionnels, des tactiques telles que la macro-expansion, l'inline et la définition de nombreuses méthodes ont été définies, afin que de nombreuses méthodes ne soient pas écrasées par des sous-classes, ce qui rend le lien statique. La VM Hotspot adopte une autre approche en incluant des fonctions de manière dynamique pour produire plusieurs versions compilées et optimisées dépendantes du contexte d'une partie donnée d'une application. La plupart des petites fonctions, telles que les paires get / set, peuvent être insérées de manière triviale, éliminant ainsi complètement la surcharge liée à l'appel de la méthode. Des méthodes de plus en plus complexes peuvent être intégrées, ce qui permet de fusionner des variables de boucle et d’optimiser des blocs de code plus volumineux. Il peut être prouvé que certains types d’objets se décomposent en un ensemble de champs et de méthodes (c’est-à-dire que leur référence n’est jamais explicitement utilisée pour tester l’identité), auquel cas les champs peuvent être alloués dans la pile. Ceci est similaire dans l’esprit à la puissance d’une méthode basée sur un modèle qui peut être paramétrée avec le type de modèle pendant l’exécution. Le résultat de ces optimisations est que le code Java polymorphe ou utilisant de nombreuses petites méthodes, interprété par les points chauds, peut souvent être exécuté à des vitesses comparables ou supérieures à celles du code C ++ polymorphe similaire. Actuellement, le style procédural dédié C ou C ++ peut surpasser le code Java similaire, mais même dans ce cas, Java est en train de faire des incursions. Par exemple, la bibliothèque mathématique de matrice Colt en Java a maintenant des performances comparables à celles de la bibliothèque mathématique de matrice FORTRAN. Il reste sans doute plus de travail à faire pour le calcul haut de gamme en Java, mais ce n’est plus un obstacle insurmontable à l’acceptation de Java pour la partie la plus difficile de la bioinformatique.
La bioinformatique est un domaine qui se redéfinit constamment. Certains problèmes sont clairement définis, tels que l'alignement de deux protéines à l'aide de l'algorithme de Smith-Waterman (Smith et Waterman, 1981). Cependant, de nombreux autres problèmes sont des cibles mouvantes. Il y a aussi la pression constante pour produire des résultats rapidement. Traditionnellement, cela a provoqué une polarisation entre le développement d’applications faites à la main dans des langages tels que C pour des tâches spécifiques comme les applications BLAST (Altschul, Gish et al. 1990), et l’utilisation de 'scripts jetables' rapidement développés dans des langages de script tels que Perl. et Python. En pratique, les «scripts jetables» deviennent souvent la base des pipelines d’analyse de séquence qui ont une durée de vie de plusieurs mois ou plusieurs années, et qui sont gérés par une succession d’individus. En fin de compte, ils doivent être réécrits pour améliorer les performances, pour corriger les bogues inhérents à la conception initiale ou pour permettre à l'application d'exécuter des tâches ne faisant pas partie des objectifs de la conception d'origine.
Java est un langage approprié pour le développement rapide d'applications bioinformatiques. Il peut être utilisé pour écrire les parties coûteuses en calculs et les contrôles de flux des scripts Bioinformatics. Si des bibliothèques de fonctionnalités biologiques sont développées, faciles à utiliser et à étendre, il devient alors possible de développer rapidement des scripts à jeter. Si ces applications à court terme deviennent partie intégrante des pipelines, la nature orientée objet du code Java signifie qu'il est potentiellement possible de récupérer une grande partie du code coûteux sur le plan intellectuel et d'isoler rapidement les défauts de conception.
Les compilateurs Java sont beaucoup plus pédants que les compilateurs C ou C ++, interdisant de nombreuses constructions non sécurisées pouvant générer un comportement d’exécution étrange. Par exemple, les conversions sont vérifiées dans la mesure du possible et les pointeurs arbitraires n'existent pas. L'allocation et la désaffectation de mémoire sont gérées par la machine virtuelle. Cela signifie que de nombreuses erreurs pouvant apparaître comme un blocage de programme dans d'autres langages font que le compilateur Java génère des messages d'erreur.
BioJava est conçu pour fournir les fonctionnalités nécessaires au développement rapide d’applications Java efficaces pour la bioinformatique. La conception du langage, du compilateur et de la machine virtuelle aide grandement au développement rapide d'applications robustes. BioJava s'appuie sur cette base solide en fournissant des API pour les objets et tâches biologiques courants, tels que les séquences biologiques, et en les lisant à partir de fichiers. De plus, un certain nombre de classes fournissent des fonctionnalités de base qui augmentent l’encapsulation et la robustesse du code hautement polymorphe de BioJava. La suite de ce chapitre décrit les principales classes et interfaces fournissant cette fonctionnalité et pour lesquelles j'étais seul ou principalement responsable de la conception et de la mise en œuvr
