Java: le projet Valhalla

Java: le projet Valhalla

Cet article a été publié initialement dans le magazine Programmez! #253.

Qu’est-ce que le projet Valhalla ?

Le projet Valhalla est un projet d’OpenJDK démarré en 2014 dont le but est d’apporter à la JVM une manière plus flexible et performante de définir des types aplatis pour exprimer des données pures (flattened data types).

Le but est d’aligner le fonctionnement de la JVM avec les caractéristiques des hardware modernes.

Pour cela, il définit de nouveaux types Java qui permettent de « coder comme une classe mais fonctionner comme un int ».

Problématique

Le système de type de la JVM comprend huit types primitifs (int, long, etc.), des objets (agrégats hétérogènes avec identité) et des tableaux (agrégats homogènes avec identité).

Des données qui ne peuvent pas être codées via un type primitif (par exemple un point avec coordonnées x et y), seront codées via un objet.
Hors les objets sont stockés dans la heap, ont un header, et doivent être référencés via une indirection en mémoire.

Prenons l’exemple d’un tableau de Point, nous aurons l’agencement suivant en mémoire :

Pour parcourir un tableau de 3 points nous devrons lire 4 headers d’objet et réaliser 3 indirections en mémoire. Les indirections en mémoire impliquent que la donnée n’est pas forcément localisée au même endroit. Les architectures mémoire modernes étant optimisées via leurs nombreux caches pour de la lecture co-localisée, la performance de lecture peut s’en trouver grandement impactée.

Le but du projet Valhalla est de pouvoir avoir un agencement en mémoire compact et sans indirection pour un tableau de point, comme on en a pour un tableau d’int. C’est un agencement aplati (flattened) et dense car sans header autre que celui du tableau.

Pour réaliser cela, le projet Valhalla va proposer deux nouveaux types qui sont Value Class et Primitive Class. Et comme corollaire l’unification des génériques entre primitifs et classes.

Value Class

Une Value Class est un nouveau type de classe qui est immuable (tous ses champs sont final), final, et n’a pas d’identité.

Un objet qui n’a pas d’identité ne peut être utilisé comme moniteur pour la synchronisation. La JVM (et le JIT) pourra donc instancier ces objets sans header, et les allouer en ligne (inline) si possible, pour éviter les indirections en mémoire.

Une instance de Value Class peut être nulle.
Comparer des instances de Value Class avec == compare la valeur de leurs champs.

Exemple de définition d’une Value Class :

public value class Point {
    int x;
    int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Un record pourra aussi être une Value Class.

Une Value Class étend java.lang.ValueObject qui lui-même étend java.lang.Object, les classes ayant une identité (donc celle sans le modifier value) étendront elles java.lang.IdentityObject qui lui-même étend java.lang.Object.

Plus d’information dans la draft JEP : https://openjdk.java.net/jeps/8277163.

Primitive Class

Une Primitive Class est un type de Value Class qui permet la définition de nouveaux types primitifs.

Comme une Value Class, elle est immuable (tous ses champs sont final), final, et n’a pas d’identité. Restriction supplémentaire, il ne peut avoir de champs d’un type qui dépend de lui-même (donc d’un type de sa hiérarchie).

Cette restriction permet pour les Primitive Class d’être représentées en mémoire en ligne (inline) sans indirection car leur forme (layout) est fixe, sans cycle.

Une instance d’une Primitive Class, comme une instance d’un type primitif, ne peut être nulle. Ce qui veut dire qu’elle a une valeur initiale, qui est une instance spéciale dont tous les champs ont comme valeur leur défaut : nulle pour les références ou la valeur par défaut de chaque type primitif (0 ou false).

Comparer des instances de Primitive Class avec == compare la valeur de leurs champs.

Exemple de définition d’une Primitive Class :

public primitive class Point {
    int x;
    int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Les Primitive Class sont monomorphiques, elles appartiennent à un unique type à la compilation et au runtime. Mais comme elles peuvent participer à un polymorphisme via une interface, il est parfois nécessaire de manipuler une référence de ces primitifs.

Chaque Primitive Class a donc une Reference Class associée (qui est une Value Class) qui peut être utilisée via Point.ref dans notre exemple. Lors de l’utilisation de cette Reference Class, l’instance peut être nulle. Cette Reference Class permet aussi d’utiliser des Primitive Class dans du code qui n’a pas été prévu pour les supporter. Par exemple, si vous appelez une méthode qui s’attend à avoir en paramètre une classe ayant une référence, vous pourrez quand même utiliser votre Primitive Class et sa référence sera automatiquement utilisée. On voit ici l’approche compatibilité amont de Java.

La JEP 402 propose de transformer les wrappers existants des types primitifs (Integer, Long, Boolean, …) en Primitive Class.

Plus d’information dans la JEP 401.

Unified Generic

Avec l’unification des types référence et primitifs grâce aux Primitive Class, le fait de ne pouvoir utiliser de type primitif dans les types génériques devient de plus en plus limitant car ces nouvelles Primitive Class ne pourraient être utilisées comme un type paramétré pour une classe ou une méthode générique.

De plus, cette limitation actuelle a entraîné une prolifération de classe spécialisée pour un type primitif en sus de la classe générique (OptionalInt, IntStream, …) ou la prolifération de méthodes spécifiques (Arrays.binarySearch(Object []a, Object key), Arrays.binarySearch(int []a, int key), …).

L’unification de la généricité a pour but de permettre d’utiliser un type primitif comme type paramétré pour une classe ou une méthode générique. La JEP ne mentionne pour l’instant que l’utilisation des Primitive Class et pas des primitifs existant.

Plus d’information dans la draft JEP : https://openjdk.java.net/jeps/8261529.

Specialised Generic

Cette fonctionnalité est pour l’instant rarement évoquée et n’a pas encore de définition claire. L’idée est de pouvoir avoir un type générique qui a certaines méthodes spécialisées pour une certaines valeur du type paramétré.

Imaginons la classe suivante:

public class Container {
    public add(T another) {
        // do something clever here
    }
}

La spécialisation d’un générique est la possibilité de définir une implémentation spécifique de la méthode add pour certaines valeurs du type paramétré T de manière transparente pour l’utilisateur de la classe Container.

Conclusion et spéculation

Le projet Valhalla, en permettant une optimisation de l’empreinte mémoire et un accès plus rapide à de la donnée, est un des projets les plus innovants et attendus d’OpenJDK.

La nouvelle API Vector (voir la JEP 417) permettant du calcul matriciel via instruction SIMD (Single Instruction Multiple Data) devrait en tirer grandement parti, et grâce à ces deux fonctionnalités réunies, Java va devenir pertinent dans des situations où le C était encore traditionnellement utilisé (machine learning, deep learning, …).

Spéculation : j’aimerais voir arriver les premières JEP (Value Class et Inline Class) pour Java 20 (au jour d’écriture de l’article, aucune JEP n’est prévue pour Java 20), croisons les doigts ! Quoi qu’il en soit, ces JEPs seront intégrées tout d’abord en preview feature, et nous ne les verrons certainement pas en version finale avant la prochaine LTS qui sera Java 21, et plus certainement pour la suivante (java 25) au vu du nombre de JEP à implémenter.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.