ne
adaptation .NET réalisée par Microsoft de l'application phare J2EE, le PetStore,
s'est avérée 10 fois plus performante que la même application écrite en Java et testée sur un serveur d'application J2EE (Oracle en l'occurrence). Le graphique suivant le prouve !

Cette déclaration tonitruante, publiée sur le site www.gotdotnet.com, a déjà fait couler beaucoup d'encre. Fidèles à notre habitude, nous avons voulu nous faire notre propre opinion en effectuant un Benchmark consistant à reprendre les principaux éléments de l'architecture J2EE et en les testant avec .NET. Vous trouverez donc dans cet article : des algorithmes écrits en Java et C#, de petites applications graphiques en JavaSwing et WinForms, des WebServices et l'utilisation des middlewares .NET Remoting, RMI sur IIOP (Corba) et RMI sur JRMP (Le protocole de Sun).
Au fur et à mesure que nous implémentions ces tests, nous nous sommes rendus compte de l'étendue du travail et de l'importance des résultats auxquels nous étions confrontés. Nous publierons donc certainement un deuxième article, complémentaire à celui-ci, dont l'objectif sera de tester les autres composantes des plateformes .NET et J2EE telles que les sites Web, les serveurs d'applications (Serviced Component et EJB), les outils du monde XML, XSL, etc ... Mais pour l'heure, nous allons comparer les fondements des langages et des bibliothèques (Framework) des mondes C#.NET et Java/J2EE.
Nous avons réalisé ces tests de la manière la plus objective possible, en essayant d'user de toutes les ficelles, trucs et astuces de J2EE et .NET afin de tirer le meilleur de chaque monde. Toutefois, il se peut que nous soyons passés à côté d'une optimisation importante qui fausserait la lecture de nos résultats. Nous vous invitons donc à réagir à cet article et à nous aider à rendre ce comparatif le plus neutre possible.
Tous les tests de cet article sont réalisés sur un ordinateur banalisé doté d'un processeur Intel PIII de fréquence 1,133 Ghz, de 512 Mo de RAM, et du système d'exploitation Windows 2000 Professional Service Pack 2. Le but n'est pas de mettre en place un laboratoire complexe basé sur des serveurs multiprocesseurs, mais de fournir des résultats conformes à une plateforme classique et surtout identique pour .NET et J2EE.
Les programmes Java fonctionnent sur la machine virtuelle de Sun Microsystems en version 1.4.1 (SDK 1.4.1) pour Windows; les applications .NET sur le CLR sont en version 1.0.3705.
Prenons un algorithme type, et essayons de l'implémenter à la fois en C# et en Java.
Remplissons un tableau de n éléments numériques (double), et effectuons le calcul du minimum de ces nombres. L'implémentation Java est triviale :
public final class TestTableaux {
private double[] valeurs;
public TestTableaux(int nombreElements) {
valeurs = new double[nombreElements];
for (int i=0; i<valeurs.length; i++){
valeurs[i] = (Math.random() - 0.5) * Double.MAX_VALUE;
}
}
public double min(){
double minimum = Double.MAX_VALUE;
double longueur = valeurs.length;
double valeurCourante = 0;
for (int i=0; i<longueur; i++){
valeurCourante = valeurs[i];
if (valeurCourante < minimum){
minimum = valeurCourante;
}
}
return minimum;
}
public long testMin(int nbItérations){
long start = System.currentTimeMillis();
for (int i=0; i<nbItérations; i++){
min();
}
long end = System.currentTimeMillis();
return (end - start);
}
public static void main(String[]
args) {
if (args.length < 2){
System.out.println
("Usage : TestTableaux nbValeursDsTableau
nbIterationsPourMoyenne");
return;
}
TestTableaux tt = new TestTableaux(Integer.parseInt(args[0]));
System.out.println("Temps écoulé : " +
tt.testMin(Integer.parseInt(args[1])));
}
}
TestTableaux.java
Le code C# équivalent ressemble énormément au code Java :
using System;
public sealed class TestTableaux
{
private double[] valeurs;
public TestTableaux(int nombreElements)
{
valeurs = new double[nombreElements];
Random r = new Random();
for (int i=0; i<valeurs.Length; i++)
{
valeurs[i] = (r.NextDouble() - 0.5) * Double.MaxValue;
}
}
public double min()
{
double minimum = Double.MaxValue;
double longueur = valeurs.Length;
double valeurCourante = 0;
for (int i=0; i<longueur; i++)
{
valeurCourante = valeurs[i];
if (valeurCourante < minimum)
{
minimum = valeurCourante;
}
}
return minimum;
}
public double testMin(int nbItérations)
{
DateTime start = DateTime.Now;
for (int i=0; i<nbItérations; i++)
{
min();
}
TimeSpan ts = DateTime.Now.Subtract(start);
return ts.TotalMilliseconds;
}
public static void Main(String[] args)
{
if (args.Length < 2)
{
Console.WriteLine
("Usage : TestTableaux nbValeursDsTableau
nbIterationsPourMoyenne");
return;
}
TestTableaux tt = new TestTableaux(int.Parse(args[0]));
Console.WriteLine("Temps écoulé : " + tt.testMin(int.Parse(args[1])));
}
}
TestTableaux.cs
Nous avons compilé le code Java en mode optimisé (javac -O). Quant à C#, nous nous sommes attachés à le compiler d'abord en mode normal, puis en intégrant les optimisations pour enfin appliquer l'outil ngen sur l'exécutable. Chaque exécutable a été testé en faisant varier la taille des tableaux parcourus de 100 éléments à 1 000 000. Les résultats sont donc présentés en mode normal et en mode logarithmique :


Les performances pures de C# sont environ 2.7 fois meilleures que celles de Java pour une manipulation "bas niveau" d'un tableau de numériques.
Le plus inquiétant, c'est que ce facteur est quasiment le même quelle que soit la taille du tableau en question. Il est également à noter que l'optimisation par le compilateur C# a un impact notable sur les performances (quasiment 50%), alors que l'utilisation de ngen nuit à l'exécution de notre petit programme (perte
d'environ 25 % des performances) !
Remplissons une collection de n éléments numériques (double), et effectuons le calcul du minimum de ces nombres.
Comme précédemment, l'implémentation Java est relativement simple. Notez toutefois qu'il est du ressort du développeur, en Java, d'effectuer le boxing / unboxing lors du passage d'un type primitif (un double ici) stocké sur la pile à un type objet (Double) stocké sur le tas :
import java.util.*;
public final class TestListes {
private List valeurs;
public TestListes(int nombreElements) {
valeurs = new ArrayList();
for (int i=0; i<nombreElements; i++){
valeurs.add(new Double((Math.random() - 0.5) * Double.MAX_VALUE));
}
}
public double min(){
double minimum = Double.MAX_VALUE;
double valeurCourante = 0;
Iterator i = valeurs.iterator();
while (i.hasNext()){
valeurCourante = ((Double) i.next()).doubleValue();
if (valeurCourante < minimum){
minimum = valeurCourante;
}
}
return minimum;
}
public long testMin(int nbItérations){
long start = System.currentTimeMillis();
for (int i=0; i<nbItérations; i++){
min();
}
long end = System.currentTimeMillis();
return (end - start);
}
public static void main(String[] args) {
if (args.length < 2){
System.out.println
("Usage : TestListes nbValeursDsTableau nbIterationsPourMoyenne");
return;
}
TestListes tl = new TestListes(Integer.parseInt(args[0]));
System.out.println("Temps écoulé : " + tl.testMin(Integer.parseInt(args[1])));
}
}
TestListes.java
Le code C# équivalent est notablement plus simple au niveau du boxing / unboxing car ce langage traite ce problème de manière transparente :
using System;
using System.Collections;
public sealed class TestListes
{
private IList valeurs;
public TestListes(int nombreElements)
{
valeurs = new System.Collections.ArrayList();
Random r = new Random();
for (int i=0; i<nombreElements; i++)
{
valeurs.Add((r.NextDouble() - 0.5) * Double.MaxValue);
}
}
public double min()
{
double minimum = Double.MaxValue;
foreach (double valeurCourante in valeurs)
{
if (valeurCourante < minimum)
{
minimum = valeurCourante;
}
}
return minimum;
}
public double testMin(int nbItérations)
{
DateTime start = DateTime.Now;
for (int i=0; i<nbItérations; i++)
{
min();
}
TimeSpan ts = DateTime.Now.Subtract(start);
return ts.TotalMilliseconds;
}
public static void Main(String[] args)
{
if (args.Length < 2)
{
Console.WriteLine
("Usage : TestTableaux nbValeursDsTableau nbIterationsPourMoyenne");
return;
}
TestListes tl = new TestListes(int.Parse(args[0]));
Console.WriteLine("Temps écoulé : " + tl.testMin(int.Parse(args[1])));
}
}
TestListes.cs
Nous avons procédé aux mêmes compilations et optimisations automatiques que dans le test précédent. A nouveau, les résultats sont saisissants :


Même s'ils sont plus mitigés que précédemment : ces
résultats démontrent que Java tire son épingle du jeu pour de petits volumes de listes (jusqu'à 10 000 éléments), pour lesquels ses performances sont meilleures que celles de C# d'environ 3 %. Autant dire que les deux langages se valent quasiment à ce niveau-là. Par contre, dès que le volume devient important (100 000 éléments et plus), les performances pures de Java deviennent dramatiques : à 1 000 000 éléments, C# va
30% plus vite que Java pour déterminer le minimum de la liste.
A noter que la taille du tas alloué par défaut par la JVM et le CLR n'a pas
été modifié car ces volumes ne nécessitent en théorie aucun Swap, cela
aurait été pertinent dans le cadre d'une dizaine de Millions d'éléments.
Il nous est apparu trop complexe ou trop subjectif d'établir un comparatif global sur les applications graphiques utilisateur. En effet, comment quantifier la fluidité, les temps de réponse d'une IHM ? Donc nous nous sommes contentés ici de développer la même application en C# - Windows Forms d'un côté, et en Java - Swing de l'autre, et de mesurer le temps de lancement de l'application, c'est-à-dire la durée qui s'écoule entre le déclenchement et l'apparition à l'écran de notre application.
Nous voulions une application à la fois simple, et représentative des possibilités offertes par les deux bibliothèques (WinForms et Swing). Nous avons choisi de développer un système simple de gestion du personnel de DotNetGuru : chaque Guru y est enregistré, photo comprise. Voilà le résultat en Java Swing :

Et l'équivalent en Windows Forms :

Le chargement de l'application graphique n'est qu'un paramètre, direz-vous, mais c'est la première impression que laisse une application graphique. Développeurs Java, combien de fois avez-vous pesté contre NetBeans ou JBuilder qui mettent souvent plusieurs dizaines de secondes à apparaître à l'écran ?
Quoi qu'il en soit, notre petite application ne laisse aucune équivoque : l'application graphique C# WinForms apparaît à l'écran 6,4 fois plus vite que son équivalent Java Swing comme en témoigne le graphique suivant :

Nous avons toutefois été étonnés de découvrir que,
encore une fois, l'utilisation de ngen ne changeait pas les performances globales de notre application.
Dans une relation client-serveur utilisant les WebServices, il est très difficile d'affirmer quelles sont les activités les plus consommatrices de temps : s'agit-il du pliage / dépliage (marshalling / unmarshalling) entre la représentation mémoire des données et la représentation en XML, ou de l'initialisation de la connexion réseau ? Passe-t-on le plus clair de son temps dans les couches réseau, ou au contraire dans le code métier ?
Plutôt que d'être exhaustifs, nous avons préféré mesurer les performances globales du même algorithme invoqué par le biais des WebServices. C'est-à-dire que nous avons mesuré le temps total entre l'invocation du service (à travers un proxy) et l'obtention du résultat par le client. Voyons le détail des activités que cela comprend :
L'algorithme que nous avons choisi est exactement le même que lors de la comparaison des langages de programmation : le calcul du minimum dans un tableau de double. Nous avons donc procédé à son implémentation sur le framework .NET sous la forme d'un fichier ASMX et de son CodeBehind :
using System.ComponentModel;
using System.Web;
using System.Web.Services;
namespace DngServices
{
public class Calculateur : System.Web.Services.WebService
{
private IContainer components = null; [WebMethod]
public double min(double[] valeurs)
{
double minimum = double.MaxValue;
double longueur = valeurs.Length;
double valeurCourante = 0;
for (int i=0; i<longueur; i++)
{
valeurCourante = valeurs[i];
if (valeurCourante < minimum)
{
minimum = valeurCourante;
}
}
return minimum;
}
}
}
DotNetService.asmx.cs
Nous avons ensuite également transposé cet exemple en Java sur le produit Apache Axis. Il existe deux choix de déploiement avec ce produit : soit sous la forme d'un simple fichier texte (contenant du code source Java, et possédant l'extension JWS), soit sous la forme d'une classe précompilée que l'on appelle un "Handler" dans Axis, ou un "Servant" dans la terminologie JAX-RPC. Nous avons donc implémenté et testé les deux, mais le code Java reste le même que dans les exemples précédents.
Enfin, vus les résultats de Axis (vous comprendrez dans quelques secondes), nous avons décidé de donner une seconde chance à Java en implémentant toujours le même algorithme sur le produit Glue (www.themindelectric.com). Pour être le plus efficace possible, nous avons même opté pour l'option de déploiement "intégrée" de glue : le processus du serveur de WebServices héberge un mini serveur HTTP, ce qui minimise les communications et les couches côté serveur. Le code est très simple, jugez par vous-même :
public interface IGlueMin{
public double min(double[] tab);
}
/***************************/
import electric.registry.Registry;
import electric.server.http.HTTP;
public class GlueMinImpl implements
IGlueMin{
public double min(double[] tab){
// Idem précédemment}
public static void main(String[]
argv) throws Exception{ HTTP.startup( "http://localhost:8000/glue" );
Registry.publish( "min", new GlueMinImpl() );
}
}
GlueMinImpl.java
Bien entendu, pour tester nos deux WebServices, nous avons utilisé le même client (écrit en C#) pour avoir la garantie de tester seulement les performances du WebService. Les résultats sont les suivants :
| Invocation du service (ms) | Java Axis (JWS) | Java Axis (classe) | .NET CodeBehind | Java Glue |
| Tableau de 1 élément | 358,5 | 354,5 | 4,5 | 5,5 |
| Tableau de 10 éléments | 360,5 | 357,5 | 4,5 | 6 |
| Tableau de 100 éléments | 368,5 | 365,5 | 8 | 11 |
| Tableau de 1000 éléments | 547,7 | 541,4 | 38 | 115,1 |
| Tableau de 10000 éléments | 2300,3 | 2285,2 | 511,7 | 970,3 |
| Tableau de 100000 éléments | 17557,2 | 17478 | 3374,8 | 8010,5 |

Notre grand étonnement est venu du produit Apache Axis qui affiche des performances (par JWS ou par la classe précompilée) 4 à 100 fois moins bonnes que celles de .NET. A cet égard, le comparatif entre .NET et Glue semble avoir plus de sens, en tous cas pour les méthodes dont le volume des paramètres est faible (jusqu'à 100 éléments dans notre tableau) : Java Glue a des performances globales qui varient de l'ordre de 20% à 40% en déperdition par rapport aux WebServices .NET. Par contre (comme nous l'avons déjà noté dans d'autres articles), plus le nombre d'éléments de notre tableau est important, plus les écarts se creusent; ils atteignent un facteur 2,5 pour un tableau de 100 000 éléments ! Mais nous sommes convaincus que les cas d'utilisation les plus commun des WebServices mettront en oeuvre des invocations de fonctions dont les paramètres et types de retour sont assez concis, du moins dans un premier temps.
La comparaison des trois middlewares mythiques que sont RMI, CORBA et DCOM est un sujet critique qui a donné lieu à de nombreux débats d'experts. La question est remise au goût du jour puisque .NET Remoting n'est pas compatible avec DCOM (il utilise un protocole binaire différent, toujours sur TCP), et RMI peut aujourd'hui fonctionner à la fois sur son protocole initial (JRMP) et sur le protocole de CORBA (IIOP).
Ce test se propose de mettre en oeuvre le même algorithme que précédemment, le calcul du minimum d'un tableau, à la fois sur .NET Remoting (sur un canal TCP et un formateur binaire), sur RMI/IIOP et RMI/JRMP (nous avons utilisé pour la partie Java les implémentations par défaut de IIOP et JRMP incluses dans la machine virtuelle de Sun Microsystems).
L'implémentation d'un serveur de calcul en C# sur .NET Remoting est très simple, encore plus que ne l'était l'implémentation d'un composant DCOM à l'époque (surtout en termes de déploiement) :
public interface IGlueMin{
public double min(double[] tab);
}
/***************************/
import electric.registry.Registry;
import electric.server.http.HTTP;
public class GlueMinImpl implements
IGlueMin{
public double min(double[] tab){
// Idem précédemment}
public static void main(String[]
argv) throws Exception{ HTTP.startup( "http://localhost:8000/glue" );
Registry.publish( "min", new GlueMinImpl() );
}
}
GlueMinImpl.java
L'implémentation du même serveur en Java sur RMI est tout aussi triviale :
public interface IRmiJRMPServeur extends java.rmi.Remote {
public double min(double[] tab) throws
java.rmi.RemoteException;
}
/************************************/
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.net.MalformedURLException;
public class IRmiJRMPServeurImpl extends java.rmi.server.UnicastRemoteObject implements IRmiJRMPServeur {
public IRmiJRMPServeurImpl() throws RemoteException {
super();
}
public double min(double[] valeurs) throws java.rmi.RemoteException {
double minimum = Double.MAX_VALUE;
double longueur = valeurs.length;
double valeurCourante = 0;
for (int i=0; i<longueur; i++){
valeurCourante = valeurs[i];
if (valeurCourante < minimum){
minimum = valeurCourante;
}
}
return minimum;
}
public static void
registerToRegistry(String name, Remote obj, boolean
create) throws RemoteException, MalformedURLException{
try { Naming.rebind(name, obj);
} catch (RemoteException ex){
if (create) {
Registry r = LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
r.rebind(name, obj);
} else throw ex;
}
}
public static void main(String[]
args) throws Exception{
IRmiJRMPServeurImpl obj = new IRmiJRMPServeurImpl();
registerToRegistry("IRmiJRMPServeurImpl", obj, true);
}
}
IRmiJRMPServeurImpl.java
Nous ne reproduisons pas ici l'implémentation du serveur sur RMI / IIOP car il suffit de changer de squelette générique : un remplacement de la classe de base UnicastRemoteObject par PortableRemoteObject suffit donc pour que le serveur change de protocole d'échange.
| Invocation d'un serveur distant par middleware (ms) | .NET Remoting | RMI / JRMP | RMI / IIOP |
| Tableau de 1 élément | 0,55 | 0,45 | 1,7 |
| Tableau de 10 éléments | 0,55 | 0,5 | 1,8 |
| Tableau de 100 éléments | 0,55 | 0,55 | 1,8 |
| Tableau de 1000 éléments | 0,9 | 1,2 | 2,9 |
| Tableau de 10000 éléments | 200 | 7,51 | 13,82 |
| Tableau de 100000 éléments | 2300 | 101,2 | 153,42 |
Ces résultats sont bien plus explicites sous la forme d'un graphique logarithmique :

On peut noter dans notre petit exemple que .NET Remoting et Java RMI sur JRMP ont des performances équivalentes pour les tableaux de petit volume (jusqu'à 1000 éléments). Dans cette gamme, RMI sur IIOP se comporte moins bien (environ 3 fois plus lent).
Au-delà de ce volume, les performances de RMI / JRMP se dégradent progressivement et semblent rejoindre celles de RMI / IIOP pour des volumes de données importants en paramètre. Par contre, .NET Remoting ne semble pas du tout prévu pour supporter ce genre d'utilisation : ses performances se dégradent très vite, pour devenir environ 23 fois moins bonnes que RMI / JRMP.
Les résultats que nous présentons ici correspondent à une moyenne arithmétique sur un ensemble d'exécutions bien entendu. Pour les autres tests, tous les résultats se situaient dans un voisinage proche de la moyenne; mais pour celui de .NET Remoting avec un tableau de 10 000 ou 100 000 éléments, il s'est passé une chose étrange : de temps en temps, les performances étaient étonnamment bonnes (environ une fois sur 5 ou 10 tests), pour retomber la fois suivante sur un résultat moyen. Nous vous laissons le soin d'imaginer une explication rationnelle à ce comportement surprenant ...
Nous espérons vous avoir convaincus de l'objectivité de notre démarche. Lors de cette étude, nous souhaitions réellement nous rendre compte des performances respectives des plateformes J2EE et .NET sur des exemples certes partiels, mais qui apportent des éléments de réponse significatifs.
Faisons le point :
Bien entendu, notre test est critiquable à plusieurs égards :
Après les déclarations diverses que nous avons tous pu lire dans la presse, où .NET battait J2EE à 26 contre 1, puis 10 contre 1, nous y voyons maintenant un peu plus clair. Il est indéniable que .NET est plus efficace que J2EE pour la majorité des parties que nous avons testé ici, d'un facteur global de 2,5. C'est déjà énorme : Java a du souci à se faire si les performances pures sont considérées comme un critère majeur dans le choix d'environnement par les décideurs et les particuliers ...
Enfin, nous invitons nos lecteurs à compléter ce test par de petites expérimentations personnelles, et à alimenter le débat des performances de nos deux plate-formes préférées. De notre côté, nous essaierons de continuer cet effort de comparaison objective et systématique sur d'autres sujets à l'avenir, tels que les composants métier, les sites Web, l'accès aux bases de données, et les outils XML. Cela promet de belles surprises en perspective.
Auteur : Thomas GIL
Copyright : DotNetGuru © Octobre 2002 - Nous vous rappelons que l'utilisation de cet article à des fins commerciales est strictement interdite sans l'accord préalable de son auteur.
Ressources
Téléchargez ci-dessous le fichier Excel contenant l'ensemble des résultats ainsi que les Matrices, n'hésitez pas à personnalisez vos graphiques pour une meilleure compréhension.
Le Fichier Excel : Results.xls (mis à jour après modification des collections Java)
Téléchargez les codes sources
TestListes.cs TestListes.java (les autres fichiers sources seront bientôt disponibles, le temps de les packager)
GuruPersonnel.cs et GuruPersonnel.java (la partie Swing vs WinForms)