Greg's Devblog Par un développeur, pour les développeurs

6jan/116

Android 3.0 enfin présenté !

Ca fait longtemps qu'on l'attendait, à tel point qu'hier au boulot on se disant ne pas comprendre la stratégie de Google sur Android, avec un système pas du tout adapté aux tablettes et une relative opacité dans le développement (pour un projet OpenSource).

Et bien voilà, ce matin est arrivée une belle vidéo illustrant ce que sera Android 3.0 (Honeycomb), et je dois avouer que contrairement à ce que je pensais, je ne suis pas déçu, bien au contraire ! Les fameuses 4 touches d'Android (parfois 3), à savoir Back, Menu, Home, et Search ne seront effectivement plus obligatoires, car intégrées directement dans l'interface. L'iPad n'a qu'à se tenir !

Sans plus attendre, je vous laisse découvrir la vidéo :

Et vous, vous en pensez quoi ?

Via googlesystem.blogspot.com

30déc/103

Algorithme du lièvre et de la tortue

En cours il y a quelques temps un prof nous a demandé si on savait comment détecter des cycles en utilisant un espace mémoire en O(1). Pour illustrer pourquoi on peut avoir envie de ça, imaginons que l'on cherche à trouver 2 valeurs qui vont avoir le même hashé (avec la méthode de votre choix).

On peut alors s'amuser à prendre 1 valeur au hasard, trouver son hashé, et ensuite utiliser cette nouvelle valeur comme valeur de départ, prendre son haché, etc... Là, on est sûr de trouver, au bout d'un temps potentiellement très long, une boucle (ou un cycle), comme sur l'image suivante (prise de wikipedia) :

La façon naïve de faire va être de stocker chacune des valeurs, et de comparer la nouvelle valeur avec toutes les valeurs précédentes. Si on veut éviter d'exploser en temps, on peut utiliser des énormes tables de hash. Sauf que voilà, il faut stocker toutes les valeurs, et on est donc en O(n) pour l'espace mémoire, n étant le nombre de valeurs avant qu'on ne cycle.

Je repose donc la question : comment peut-on effectuer le même traitement en O(1) pour l'espace mémoire ?
Pas forcément intuitive, l'algorithme est au final particulièrement simple et élégant. Il a été défini par Floyd, et on le connait sous le nom d'algorithme du lièvre et de la tortue.

On peut écrire les opérations que l'on fait pour avancer sur la chaine sous la forme ai+1 = f(ai), f étant la fonction qu'on utilise (fonction de hash dans l'exemple que j'ai donné). Mais au lieu d'itérer une seule fois sur la chaine, on va en fait lancer 2 itérations en parallèle :

  • La tortue avance lentement, donc à chaque itération elle fait ai+1 = f(ai)
  • Le lièvre est très rapide, il va donc avancer 2 par 2, avec bi+1 = f(f(b1))

De façon évidente, le lièvre va arriver dans le cycle avant la tortue, et va se retrouver à tourner dedans en attendant que la tortue n'arrive... Une fois que la tortue est dans le cycle, on a la garantie qu'elle va arriver à la même valeur que le lièvre au bout d'un temps donné, puisque qu'en fait le lièvre rattrape la tortue d'1 étape à chaque itération. Quel que soit l'écart entre les 2, une fois que la tortue arrive dans le cycle le lièvre sera sur le même noeud en au plus L itérations (L étant la taille du cycle).

Et du coup, on n'a besoin d'aucune mémoire supplémentaire pour savoir où est le cycle !
Le pseudo-code, pris de wikipedia, est le suivant :
mu := 0;
répéter
  mu := mu+1;
  x := f(x);
  y := f(f(y));
tant que x <> y

On peut difficilement faire plus simple...

Le seul défaut de cette méthode est la complexité : on reste en O(n), mais on va tout de même appeler 3*n fois la fonction f. Mais cette constante reste relativement faible par rapport au gain en mémoire (on passe de O(n) à O(1)...).

Plus d'informations sur wikipedia

Taggé comme: , , 3 Commentaires
18déc/100

Interrupted System Call

Hier j'essayais de terminer un petit player mp3 sur nos appareils eInk, mais au démarrage du mp3 ça quittait toujours brusquement. Le code récupéré des taiwanais utilise libmad, et pour faire simple, le principe est le suivi :

  • On ouvre un flux sur "/dev/dsp"
  • On le configure
  • Libmad décompresse le mp3 petit à petit, et à chaque morceau décompressé on le balance dans /dev/dsp
  • On referme le flux quand on a fini...

Parfois ça quittait dès le départ, et parfois on avait genre 1/4 de seconde du mp3 avant que ça ne quitte... On a rapidement repéré à quel endroit ça quittait : au moment d'écrire dans /dev/dsp (par blocs de 8192 octets), ça s'arrêtait très rapidement avec un message d'erreur : "Interrupted System Call".

Alors, à quoi correspond ce message ? En fait c'est tout simple : on a fait un appel système (ici, l'écriture dans /dev/dsp) qui a été interrompu par un signal... L'écriture dans /dev/dsp n'est en fait pas une opération atomique, donc on n'a aucune garantie qu'elle puisse se dérouler complètement en une seule fois.

La solution a mettre en place est assez basique : au lieu de quitter à la moindre interruption, on note combien d'octets ont déjà été écrits, on incrémente le pointeur d'autant, on décrémente la taille à écrire, et on relance le fwrite... Et ça marche !

Merci au site suivant pour la petite explication claire et précise : http://book.chinaunix.net/special/ebook/addisonWesley/APUE2/0201433079/ch10lev1sec5.html

Note au passage : c'est marrant de voir comment tout est flux sur Unix... Du coup, pour jouer un son, c'est aussi un simple flux dans lequel on envoie les données du son à jouer. On peut même faire un "cat toto.wav > /dev/dsp" et entendre son wav :p

18déc/102

Temps des différents accès

L'année dernière, en "Compilation Avancée", on a vu différentes méthodes permettant de réduire les défauts de cache, ce qui permet de gagner un bon gros facteur en terme de performances.

Alors certes, pourquoi pas, mais on se rend compte qu'en règle général on a du mal à imaginer les écarts de temps... Un accès à une valeur du cache L2, est-ce vraiment beaucoup plus lent que pour le cache L1 ? Et que pour un accès en RAM ? Il existe donc un petit tableau "comparatif", histoire que tout programmeur puisse se faire une idée de l'importance de ce genre d'optimisations...
Je ne l'ai pas traduit car de toute façon c'est relativement simple à comprendre, je vous laisse donc vous instruire :)

L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns
Mutex lock/unlock 25 ns
Main memory reference 100 ns
Compress 1K bytes w/ cheap algorithm 3,000 ns
Send 2K bytes over 1 Gbps network 20,000 ns
Read 1 MB sequentially from memory 250,000 ns
Round trip within same datacenter 500,000 ns
Disk seek 10,000,000 ns
Read 1 MB sequentially from disk 20,000,000 ns
Send packet CA->Netherlands->CA 150,000,000 ns

Un petit commentaire ? A la louche, un facteur 14 entre les accès au cache L1 et les accès au cache L2, et à nouveau un facteur de cet ordre pour un accès en mémoire principale. Comme quoi ça coûte vraiment cher !

Source : The Axis of Eval

Taggé comme: , , 2 Commentaires
18déc/102

Comment reconnait-on un programmeur ?

L'autre jour sur stackoverflow quelqu'un a posé une question assez marrante : comment reconnait-on un programmeur (ie, sans connaître la personne, juste à partir de petites manies/habitudes).

J'ai regardé les réponses par curiosité, et je dois avouer que certaines me correspondent bien. J'ai testé avec ma copine, et dans l'ensemble elle approuve (avec une mention spéciale pour une réponse en particulier). Voici donc un petit panel rapide et non exhaustif :

  • Utilisation des parenthèses imbriquées (je dois admettre que je le fais assez souvent (et même dans ce billet !))
  • Temps de latence anormalement long pour répondre à des questions pourtant très simples, du style "tu veux du thé ?". Ma copine râle beaucoup à propos de ça... Certains disent que c'est parce qu'il faut le temps de faire le pre-processing, l'analyse lexicale et syntaxique, et l'optimisation de la réponse, mais pour ma part je dirai que c'est juste que quand je suis concentré sur du code (ou un domaine relatif à la programmation) je suis un peu dans un mode de particulier (logique et proche de la machine, on va dire), et que du coup, effectivement, ça interrompt un peu le fil de ma pensée et il me faut un temps pour me "reconnecter". D'ailleurs, dans ma tête, souvent je suis en train de taper du code, et là ça fait "tiens, on me parle"... je continue à taper un peu, et puis au bout d'un même je switch sur le mode "social" et je réponds. Mais ça énerve ma copine d'attendre plus d'1 seconde pour ça.
  • Numérotation qui commence à 0. Je ne comprends d'ailleurs toujours pas pourquoi dans les éditeurs de code on numérote à partir de 1...
  • Pour un programmeur, 256 est un chiffre rond. Là je ne dirai que ce n'est pas forcément vrai pour tous les programmeurs, mais en tout cas pour ceux qui font pas mal de C/C++. En Java, je le vois beaucoup moins.
  • Le fait de revenir en arrière dans la call stack de la conversation. Ca j'ai tendance à le faire et c'est vrai que les gens n'aiment pas beaucoup, dans l'ensemble. Du style si je pensais à quelque chose à dire à un moment donné et qu'entre temps quelqu'un a parlé et que la conversation à avancé, dans ma tête c'est toujours présent, et à tout moment s'il y a un blanc je vais avoir tendance à revenir en arrière dans la conversation pour repartir de là où j'avais quelque chose à dire.

Il y a pas mal d'autres réponses sympas sur le site, donc si vous avez un peu de temps à perdre, je vous invite à aller voir ça :)

Taggé comme: , 2 Commentaires
18déc/100

Call Stack en C/C++

Ces derniers temps je me suis attaché au portage d'FBReader sur différentes plateformes, et comme c'est un code assez conséquent et où j'avais besoin de voir un peu quel appel de fonction arrivait d'où (autrement que par le "find reference" d'Eclipse), j'ai cherché rapidement s'il n'y avait pas un moyen en C d'afficher des stack trace. A ma grande surprise, c'est possible !

Voici donc une petite fonction qui peut se révéler utile lorsqu'on fait du debug :)

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>

/* Obtain a backtrace and print it to stdout. */
void
print_trace (void)
{
  void *array[10];
  size_t size;
  char **strings;
  size_t i;

  size = backtrace (array, 10);
  strings = backtrace_symbols (array, size);

  printf ("Obtained %zd stack frames.\n", size);

  for (i = 0; i < size; i++)
     printf ("%s\n", strings[i]);

  free (strings);
}

Ca m'a bien dépanné, et même si je ne m'en suis pas tellement servi au final (juste une fois), je pense que ça fait parti des petits bouts de code toujours utiles à avoir sous le coude...

Pour plus de détails, je vous invite à vous rendre sur le site de GNU.

13déc/103

Code Taiwanais : comeback !

Ca faisait assez longtemps que je n'avais pas eu à jouer avec du code fait à l'étranger, et là je suis tombé sur quelques éléments intéressants à nous.

Je vais commencer par ce qui me parait être le plus sympa, alors voilà, les magic number dans toute leur splendeur... On est dans une classe "Image", qui possède des getters pour largeur et hauteur :

// fix for null images
width(){
	if (myWidth > 2000)
		return 39;

	return myWidth;
}

Je vous épargne le reste du code, on a exactement la même chose pour la hauteur :) Pourquoi cette limite à 2000 ? Et pourquoi retourner 39 dans ce cas ? Aucune idée ! J'aime beaucoup le commentaire, par contre :p

Dans un registre un peu moins grave, une partie du code qui est copiée/collée un peu partout, et qui sert à convertir une valeur sur 8 bits (de 0 à 255, pour un niveau de gris) en 4 valeurs (0x00, 0x40, 0x80, et 0xC0) :

if (pixel < 64)
	pixel = 0x00;
else if (pixel < 128)
	pixel = 0x40;
else if (pixel < 192)
	pixel = 0x80;
else
	pixel = 0xC0;
	

Alors, cette fois-ci au moins on comprend le code, à quoi il sert. Par contre, ça fait 8 lignes copiées/collées à une dizaine d'endroits différents, ce n'est pas terrible, d'autant plus que ça revient juste à retirer les bits de poids faible :

pixel &= ~63;

Je me demande si GCC est capable d'optimiser ça, d'ailleurs.

Un petit dernier pour la route, mais cette fois-ci, ce n'était pas du code taiwanais, c'était du beau code de la fac :p. Vu dans une méthode :

if (this == null){
	doSomething();
} else {
	doSomethingElse();
}

Un avis ?

Taggé comme: , 3 Commentaires
10déc/101

Chrome Easter Egg

Si vous suivez un minimum l'actualité, vous avez du voir que ça bouge du côté de Google et de Chrome OS : les testeurs ont reçu les premières machines, on commence à voir tourner la bête, et une vidéo de promotion est sortie.

Jusque là, rien d'anormale... Certes... Sauf qu'en fait, au milieu de la vidéo on voit sur un tableau une équation à la con, et du coup des personnes de chez Jamendo se sont amusées à la résoudre... Je vais vous laisser lire la suite sur leur blog, mais pour faire simple, on obtient une fraction qui peut être convertie en URL, et en allant sur cette URL... Un portable sous Chrome OS est offert ! Bon, c'est trop tard, c'était uniquement pour le premier venu, mais je trouve ça sympa d'avoir mis un easter egg en plein milieu de la vidéo.

Bravo à Google, et puis surtout bravo à ceux qui ont trouvé la réponse, il fallait le faire !

29nov/101

Algorithmes génétiques : petite voiture

Voici un petit programme flash qui montre la puissance des algorithmes génétiques : http://www.qubit.devisland.net/ga/index.html

C'est sympa car très visuel, en fait, on voit la progression au fur et à mesure, avec le petit graph pour montrer ce que ça donne.

Premier essai...

Quand je suis tombé dessus ce matin, je me suis dit que j'allais laisser tourner le programme quelques heures histoire que ça évolue bien et que ça donne une voiture super efficace. J'ai regardé de temps en temps, et voici ce que j'ai pu voir :

  • Premières générations super aléatoires, car les roues ne sont pas forcément en bas !
  • Après 2-3 générations, la majorité des voitures générées ont au moins 1 roue en bas
  • Après une dizaine de générations, les voitures comment à pas mal avancer, mais l'évolution a été bizarre : elles ont toutes une roue en bas, et les poids sont mis de façon à équilibrer la voiture pour rouler ainsi. Du coup ça n'avance pas super loin car s'il y a trop de relief ça tombe
  • A un moment est apparue la première voiture avec 2 roues en bas, qui est allée 2 fois plus loin que les autres ! Mais les mutations à la génération suivante ont fait que les 2 roues du bas étaient très mal placées, du coup elle est allée moins loin que les voitures à 1 roue et a disparu...
  • Après quelques dizaines d'évolutions, les voitures à 2 roues ont remplacé les voitures à 1 roue, en gardant une disposition des poids similaires à celle des voitures à 1 roue
  • Après un peu de temps, une voiture beaucoup plus large que les autres est apparue, elle est beaucoup plus apte a franchir les obstacles, et a imposé son génome en 2-3 générations.

Et en gros à partir de là, très peu d'évolution. Toutes les voitures sont sensiblement identiques, dès qu'une voiture un peu différente est générée, elle est moins bonne. Au final, j'étais déçu car aucune ne parvenait à passer le creux qu'on voit sur l'image au-dessus : elles se retournaient toutes.

On peut faire mieux ?

J'ai fini par me dire que ça pourrait être sympa de recommencer pour voir si l'évolution peut faire mieux. Et là, radicalement différent :

  • Première génération, une voiture à 2 roues en bas qui avance un peu
  • Deuxième génération, plusieurs voitures à 2 roues assez efficaces, le génome se répand rapidement !
  • Cinquième génération, les voitures arrivent aussi loin que celles d'avant après une centaine de générations (...)
  • Huitième génération (à la louche), les voitures passent le creux et vont plus loin ! (comme on le voit sur l'image ci-dessus)

C'est allé super vite ! En fait, du fait que les bonnes mutations soient arrivées très tôt, l'ensemble des voitures était encore composé de voitures très différentes, et le brassage a donc fait évoluer les choses très rapidement. Dans le cas précédent, tout avait évolué lentement, menant à des générations beaucoup trop homogène. C'est donc bien de la différence et de la diversité que vient la force des algorithmes génétiques.

Une fois qu'une génération est arrivée dans un optimum local, il devient très difficile d'en sortir, car toutes voiture qui est différente est potentiellement moins bonne, et donc va être oubliée... C'est d'ailleurs ce que je voyais sur le graph de progression : l'évolution se faisait par "breakthrough", comme on dit, avec de grands pas d'évolution d'un coup. Il faut qu'une voiture apporte une énorme amélioration sur les autres pour qu'elle soit gardée et que son génome soit passé à tout le monde au bout de quelques générations...

A l'occasion je m'attaquerai un peu à ce genre de programmation, je trouve ça très sympa, pas forcément difficile à faire (plus difficile à tweaker pour trouver la bonne taille de population, le bon taux de mutation, etc.), et puis visuellement c'est sympa à regarder (ça ferait un bon screensaver d'ailleurs).

29nov/100

1337 code : un blog très instructif

Ce matin, au lieu de travailler (...) je farouillais encore un peu partout et je suis tombé sur un blog qui me parait très intéressant : http://www.ihas1337code.com

Ce qu'il a d'intéressant, ce qu'il y a expose des problèmes avec des énoncés très courts, qui sont en fait globalement des problèmes d'algorithmique simple que l'on peut avoir en entretien d'embauche. Bon, moi les entretiens je m'en fous, mais les problèmes sont tous de mini-casse-têtes, intéressants à résoudre, et il y apporte des solutions souvent originales, et parfois des discussions annexes intéressantes.

Je vais surement reprendre ici certain des problèmes dans les semaines à venir. Et si jamais je suis très motivé je ferai les codes correspondants en Caml !

27nov/100

The Story of Mel

Une histoire sympa sur ce qu'est un "vrai" programmeur. Je la trouve assez poétique. http://catb.org/esr/jargon/html/story-of-mel.html

Taggé comme: Aucun commentaire
25nov/101

Premiers essais sur Cell

J'ai pu tâter un peu de la PS3 et du Cell ce matin en cours... Pas grand chose à en dire pour l'instant, on en est juste au "Hello World" sur les différents SPE. Mais pour l'instant je trouve ça sympa, et j'ai hâte de pouvoir résoudre de vrais problèmes avec pour voir ce qu'on peut en tirer niveau performances. Ca m'a l'air moins hardcore que la programmation sur GPU dans la mesure où ça revient juste à avoir 8 coeurs qui tournent de façon plus ou moins autonome, avec la possibilité de piocher dans une mémoire partagée si besoin (256ko de RAM, c'est pas énorme ^^).

Par contre, de ce que j'ai pu en voir, c'est que niveau debug ça risque d'être violent : si les données ne sont pas parfaitement alignées, ou qu'on transfert des paquets de tailles qui ne plaisent pas (genre 24 octets), alors ça va tout droit au SEGFAULT ou BUS ERROR... Pas super informatif, mais heureusement que le printf est là pour savoir ce qui se passe !

Taggé comme: , 1 commentaire
24nov/102

Remplacer π par τ ???

Je suis tombé aujourd'hui sur un article de Michael Hartl qui explique, en somme, pourquoi π n'est pas si génial que ça, et pourquoi τ serait plus approprié... Je ne vais pas traduire et remettre ici l'ensemble de sa page web, mais je dois dire que dans l'ensemble j'ai trouvé ça plutôt convaincant...

L'idée de base part du constat que π a été défini par la division de la circonférence d'un cercle par son diamètre. Pourquoi c'est une mauvaise idée ? Parce qu'en pratique on utilise toujours le rayon... Et là, on se retrouve en fait avec une constante différente, τ = 2π.

Ca peut sembler anodin comme changement, mais il y a en fait pas mal de conséquences, dont la plupart sont des simplifications de formules existantes. Un angle en radian va alors de 0 à τ au lieu de 0 à 2π, ce qui est déjà plus logique pour symboliser un tour complet.

Dans l'article il y a beaucoup d'explications autour de ça, ainsi que d'autres formules, mais il y en a une, probablement la plus connue de toutes, qui a attiré mon attention : πr2... Pour le coup, cette formule parait belle, pure, et si on voulait utiliser τ ici on se retrouverait avec un horrible (1/2)τr2... Assez moche, non ? Bah en fait, oui et non. Si on regarde les formules utilisées un peu partie et qui sont quadratiques, notamment en physique, on a des formules qui ont quelle tête ? Et bien, pour la vitesse, on a energie cinétique = (1/2)mv2... En fait, dans en gros un des seuls cas où ça parait moins beau, on peut trouver une belle raison pour utiliser là encore τ : ça permet d'harmoniser un paquet de formules...

Je vous invite à lire ce petit article, c'est sans prétention et ça se lit tout seul. Ca parait complètement surréaliste de vouloir changer une constante, surtout la constante la plus utilisée et la plus emblématique, et pourtant...

A méditer...

Taggé comme: , 2 Commentaires
20nov/100

Réutiliser du code sans héritage multiple

L'héritage multiple : pas sans problèmes

En C++, on a l'héritage multiple, qui permet de rapidement factoriser du code, mais peut poser certains problèmes de "collisions" si plusieurs classes dont on hérite implémente une méthode du même nom, ou si plusieurs classes dont on hérite héritent elle-mêmes d'une même classe. Exemple simple : je créé un PointColore, qui hérite de Point3d et de Color. Que se passe-t-il si Point3d hérite d'une classe "Triplet" (pour x, y, et z), et Color aussi (mais pour r, g, et b) ?

Et histoire de donner un exemple dans l'autre sens, on pourrait avoir une classe VoitureAmphibie qui hérite de Voiture et de Bateau, et ces 2 classes qui héritent de "Transport" (qui contient alors des coordonnées x, y, et z, par exemple). Dans ce cas, on ne veut pas avoir les données de Transport en double, mais juste en 1 exemplaire...

Bref, ils existent plein de petits ajouts à la syntaxe pour palier à ça, mais ça introduit quand même une certaine ambiguïté, et donc un risque d'erreur (tout comme la surcharge à outrance).

Sans héritage multiple : la galère

Pour les exemples ci-dessus, comment procède-t-on si on n'a pas d'héritage multiple ? C'est notamment le souci en Java, et pour résoudre cela les interfaces ont été introduites... Sauf que c'est super, ça marche bien, mais avec les interfaces on ne factorise pas du tout le code... Alors oui, on peut créer un objet de la classe dont on voudrait faire l'héritage multiple, avoir un champs de cette classe dans notre classe, et puis "binder" toutes les méthodes, ça reste un peu chiant, redondant, et lourd pour pas grand chose...

Et le PHP dans tout ça ?

Il existe probablement plein de méthodes pour palier à ce problème, et je ne les connais pas forcément. Mais en lisant un peu par hasard des informations sur la prochaine version de PHP (suite à l'abandon de PHP6 !), je suis tombé sur une solution que je trouve à la fois simple et élégante : les traits.

Un bout de code vaut mieux que de longs discours, et comme je n'ai ni le temps, ni l'envie d'en faire un moi-même, je vais tout simplement repomper celui du wiki d'où j'ai tiré ces informations...

trait ezcReflectionReturnInfo {
   function getReturnType() { /*1*/ }
   function getReturnDescription() { /*2*/ }
 }

 class ezcReflectionMethod extends ReflectionMethod {
   use ezcReflectionReturnInfo;
   /* ... */
 }

 class ezcReflectionFunction extends ReflectionFunction {
   use ezcReflectionReturnInfo;
   /* ... */
 }

C'est tout simple, on peut voir les traits un peu à l'image des classes abstraites : on peut y mettre du code, mais on ne peut pas les instancier. Et en fait, l'idée est juste que ça revient à inclure le contenu des traits directement dans la classe. Et ça permet d'intégrer du code venant de différents endroits, en gardant un héritage simple, et donc en évitant le surcoût et/ou certaines des emmerdes liées à l'héritage multiple.

Alors certes, on va me dire qu'en pratique, on a les mêmes soucis qu'avec l'héritage multiple si on inclus plusieurs traits qui ont des méthodes ayant un même nom... Les équipes derrière PHP ont pensé à tout !

 class Talker {
   use A, B {
     B::smallTalk instead A;
     A::bigTalk instead B;
     B::bigTalk as talk;
   }
 }

Je ne prends pas la peine de détailler les classes et tout car seul le bout de code ci-dessus est intéressant. Mais en gros, les traits A et B contiennent des méthodes smallTalk et bigTalk, et on a donc un petit conflit si on inclus les 2 sans rien préciser. Du coup on peut préciser quelles méthodes utiliser (la méthode smallTalk issue de B, au détriment de celle issue de A), et on peut aussi renommer une méthode (la méthode bigTalk de B) en un autre nom si on veut pouvoir l'appeler tout de même...

Au final, on obtient un système souple et flexible, qui me fait penser à un mélange entre héritage mutiple et système de macros pour inclure du code à la compilation. D'après le wiki, le système des traits existent déjà dans d'autres langages, dont Scala, donc j'y jetterai un coup d'oeil à l'occasion. Mais bon, j'étais content de trouver une solution viable à tous ces problèmes, autres que les non-solutions proposées en Java...

20nov/100

Java : lenteurs inexpliquées ?

On aurait du utiliser un profiler. Voilà la conclusion à laquelle je suis arrivé après qu'on ait cherché à améliorer la vitesse et la réactivité de notre application au boulot (pour des appareils à encre électronique, donc en gros le même hardware que sur des smartphones).

Date.toString()

Sur certains écran, l'affichage prenait 6 secondes, alors que sur des écrans équivalents on était largement en-dessous de la seconde. On n'avait pas de profiler disponible sur l'appareil, donc on a commencé à mettre des logs un peu partout pour afficher les différents temps, et là, surprise : ce qui prenait autant de temps, c'est l'affichage des dates ! Un simple date.toString() peut ainsi prendre  plus d'1 seconde, parce que par défaut ça va aller charger tout et n'importe quoi pour trouver les locales et créer de quoi formatter la date comme il faut ! La solution, qui était déjà utilisée, a été de cacher les DateFormatter histoire qu'on ne se prenne pas le surcout à chaque fois et de bien convertir les dates en String en utilisant ce qui est en cache.

Requêtes à 2 à l'heure...

C'était déjà mieux, pas encore parfait. On avait encore des lenteurs assez bizarres sur des requêtes pourtant simples : de bêtes SELECT, UPDATE, ou INSERT pouvaient prendre 1 à 3 secondes, alors que d'autres prenaient à peine quelques millisecondes...

On a donc mis des logs autour de toutes les requêtes, en séparant les moindre lignes, pour essayer d'évaluer au mieux là où on passe trop de temps. Et surprise, l'exécution des requêtes est rapide, le problème n'est pas là ! En fait si on a une méthode qui ne fait que la requête, la ligne qui exécute la requête dans la méthode est rapide, alors que l'appel de la méthode en entier est long... Pas super logique :p

Bon bah c'est simple, il faut virer tout ce qui ne concerne pas directement la requête, à savoir juste des logger.trace qui ne sont même pas affichés, puisqu'on avait mis le mode de log à "info". Et là, gain de vitesse monstrueux !

Et oui, car afin d'avoir un maximum d'informations de debug, on a des logger.trace un peu partout dans le code des requêtes, qui permettent d'afficher toutes les valeurs injectées dans la base ou récupérée depuis celle-ci. Alors certes, rien n'est affiché, mais les arguments passés aux méthodes sont tout de même évalués, et parmi ceux-si on trouve... des dates ! Le retour du foireux Date.toString()...

On a été assez étonné car au final depuis le début on était persuadé d'avoir un problème de lenteur avec la base, alors que le problème était tout autre (et dans le fond venait de nous, et non de SQLite ^^). Ca explique pourquoi on ne trouvait pas trop de monde avec le même souci sur le net.

Régler le problème des logs

J'ai trouvé ce problème un peu con, en fait. C'est un souci qu'on n'aurait pas eu en C/C++ : les méthodes de log sont des macros, et quand on ne veut pas de log, la macro ne défini plus rien, et le log n'est tout simplement plus présent dans le code compilé.

Bon, à part commenter tous les logs (un peu chiant), que peut-on faire en Java pour ne pas avoir ce souci ? On n'a actuellement pas de préprocesseur, mais peut-être faudrait-il envisager d'en mettre un petit, au moins pour les logs... L'autre solution peut être de passer à un autre logger dont un collègue m'a parlé : au lieu de prendre juste un String, il reprend en fait la syntaxe du printf avec les "Date : %s..." et l'utilisation de la date comme argument. Et là, pour le coup, si le logger n'affiche rien, les arguments sont passés mais il n'y a virtuellement rien à évaluer (les dates resteront des Object tant que le logger ne les affiche pas), aucune concaténation de String à faire, c'est beaucoup plus rapide.

Alors certes, c'est mieux, mais conceptuellement ça reste du code qui pourrait être présent dans l'exécutable final, qui ne ferait rien, et qui ne serait pourtant pas 100% gratuit. Et j'ai un peu du mal à me dire que l'exécutable finale pourrait être légèrement (à peine, mais bon) plus lent que ce qu'il pourrait être juste parce qu'on a laissé trainé des debugs dedans...

Si vous avez des solutions alternatives, je suis preneur !

Moral ?

Ce qu'on a eu là comme souci illustre aussi quelques règles de programmation qu'on a parfois tendance à négliger :

  1. Pas d'optimisation à l'aveugle : quand on cherche à optimiser, il est important d'avoir des benchs pour savoir si l'optimisation est mieux ou pas que le code d'origine, et surtout savoir si on en train d'optimiser une partie pertinente du code. J'ai ainsi fait des optimisations sur le code d'affichage qui ont eu finalement un impact minime sur les performances globales, puisque 90% de la perte de temps était ailleurs. Et c'est comme ça qu'une fois sur PocketPC j'ai passé quelques heures à faire un code d'affichage beaucoup plus optimal pour me rendre compte que je n'avais rien gagné : GCC faisait aussi bien sur le code d'origine...
  2. Se remettre en question avant de douter des autres : ça fait plusieurs mois qu'on sait que les accès à la base sont lents, et depuis le début on a rejeté le faute sur SQLite... Pourtant les bonnes pratiques veulent qu'on vérifie que son propre code n'est pas la cause des lenteurs/bugs avant de dire que c'est dans une librairie qu'on utilise.