dimanche 23 décembre 2012

Texte courbé

J'écris des logos, ces temps-ci, et je me suis donc intéressé à ce que l'on pouvait faire avec du texte, notamment pour le courber. Voici donc un petit exemple.

On démarre d'abord en virant le cube puis en passant en vue de dessus pour y voir plus clair. Ajoutons un cercle (Shift-A => Curve => Circle), puis le texte (Shift-A, Text). L'on passe en mode édition avec la touche Tab, comme pour passer en mode vertex pour un objet.

C'est maintenant que la magie opère: revenez en mode objet (Tab), assurez vous que le texte est bien sélectionné, et cliquez sur le cercle tout en maintenant Shift. Les deux objets sont maintenant sélectionnés, le texte en rouge, et le cercle en orange (si c'est l'inverse, les résultats seront plutôt ésotériques). Pressez Ctrl-P, et choisissez Curve Deform. Le texte s'installe sur le cercle!

Ensuite, il est possible de contrôler l'ajustement du texte de la façon suivante: sa position sur le cercle, en sélectionnant le cercle (et pas le texte!), et en effectuant une rotation (r + mouvements de souris). La distance au centre en sélectionnant le cercle, et en effectuant un changement d'échelle (s + mouvements de souris). Et enfin, la taille du texte, en sélectionnant le texte, et en effectuant un changement d'échelle (toujours s + souris). Notez qu'il est possible de passer le texte à l'intérieur du cercle en passant l'échelle en négatif. J'ai rajouté un autre cercle et un autre bout de texte pour compléter:

Donnons un peu d'épaisseur à l'ensemble. Par défaut, le texte n'a pas de profondeur, mais il est possible de l'extruder. Sélectionnez le texte, allez dans le menu texte (F), et mettez une valeur non-nulle pour "Extrude" dans le sous-menu Geometry.

En regardant de côté, voilà ce que cela donne:

À partir de là, un disque et un matériau un peu rigolo, et l'on a un beau logo!

samedi 22 décembre 2012

Hareng rouge

Comme disent les britanniques!

Je m'étais bêtement focalisé sur cette histoire de version du driver libre Radeon, sans me rendre compte du message d'erreur en début de boot, m'indiquant qu'il n'arrivait pas à charger un firmware. En fait, c'était ça: Debian, par défaut, n'installe pas les firmwares non libres. Une fois le paquet installé (aptitude install firmware-linux-nonfree), j'ai une accélération matérielle décente, et surtout Blender marche à nouveau!

Plus d'excuses pour vous faire partager d'autres blendereries.

lundi 17 décembre 2012

Et Blender qui ne marche toujours pas...

c'est ça de faire son malin. À force de mettre à jour mon système toutes les semaines, j'en suis arrivé à une version de Xorg pour laquelle ATI ne fournit plus de drivers propriétaires, ma Radeon HD 4850 étant considérée "trop vieille". Je me suis donc rabattu sur les drivers libres, qui ont l'avantage de m'afficher les logs de démarrage en haute résolution (yay!). Par contre, Blender apprécie moyennement, et refuse d'afficher quoi que ce soit en mode ombré en et mode texturé, ne laissant que le mode fil de fer, ce qui limite pas mal soit dit en passant. Quand à OpenRailz, il crashe au démarrage. Cela explique mon silence sur tout ce qui est 3D, ces derniers temps.

Je crois comprendre que la dernière version du driver libre corrige de nombreux problèmes. J'attendrai donc patiemment qu'elle débarque sur Squeeze (ou plus probablement Jenny, la version suivante).

mardi 27 novembre 2012

Ces jeux qui m'ont marqué - A IV Network$

Prononcez A4 Networks, il s'agit d'une simulation économique dans laquelle vous prenez la direction d'une société de transports et d'investissements. Chaque carte représente une ville en 3D isométrique, dans laquelle vous pouvez ajouter des lignes de train et de bus et construire ou acheter des immeubles, golfs et autres. Chaque carte propose un défi particulier, comme de redresser le réseau de métro de Londres, ou de construire les îles Caïman à partir de zéro.

En tant qu'aficionado des jeux de gestion ferroviaire, je ne pouvais m'en passer et me jetais dessus quasiment dès sa sortie. Au final, c'est malheureusement un mauvais jeu, ce qui ne m'a pas empêcher d'y passer des heures. Et ses qualités tant ses défauts rendent son étude tout à fait intéressante. Voyons donc d'abord là où les développeurs se sont complètement plantés.

La simulation ferroviaire est ratée

Contrairement par exemple à Transport Tycoon, où les trains sont gérés via des signaux, et qui permettaient d'atteindre une complexité étonnante, les trains dans A4 Networks sont gérés par horaires: chaque train sait l'heure à laquelle il doit quitter la gare. C'est proprement ingérable, puisqu'il est très difficile de savoir si oui ou non deux trains vont se tamponner à un aiguillage, et impossible à forcer un train a prendre un côté plutôt qu'un autre dans une route à deux voies. Assez rapidement, l'on se retrouve à faire d'abord des lignes simples en point à point, et lorsque l'on commence à vouloir placer plus d'un train par ligne, à faire des lignes circulaires, sans aucun aiguillage.

L'alternance jour/nuit est gérée, mais elle est trop rapide par rapport à la vitesse des trains, ce qui rend impossible la gestion du rush hour. L'on tente donc d'avoir une densité de trains importante, en espérant qu'ils arrivent en gare au bon moment.

Le transport des marchandises est également sans queue ni tête: les trains de marchandises se remplissent à une gare, et se vident à la suivante, mais si par exemple une usine n'a pas eu le temps de se réapprovisionner, le train repart à vide, et se met à ramener les marchandises vers l'usine! Il faut manuellement aller inverser sa direction pour repartir dans le bon sens.

La simulation économique est ratée

Le jeu est très difficile à prendre en main, car les transports rapportent extrêmement peu par rapport à leurs coûts (ce qui est réaliste, ceci dit), jusqu'au moment où l'on se rend compte que par un curieux effet, le prix des immeubles s'écroule à chaque début d'année, vous permettant d'acheter quantité de bâtiments en solde, et de les revendre à 2 ou 3 fois le prix quelques mois plus tard. Une fois compris, ensuite, que l'impôt sur les bénéfices vous mettra vite sur la paille, et qu'il faut donc toujours investir tout son cash, la stratégie gagnante devient triviale: acheter le plus possible de bâtiments, les revendre juste avant le point de chute, et tout racheter. Ne vendre (et donc générer du cash) que pour construire autre chose, par exemple vos lignes de train. Au bout de quelques années, vous dépasserez les 2 milliards de cash, moment où l'entier 32 bits contenant votre argent durement gagné se retrouve tout d'un coup très négatif, vous forçant à une faillite prématurée. Dommage, non?

Évoquons à peine le mode bourse, qui fonctionne en dépit du bon sens. Certains scénarios demandent à obtenir une participation majoritaire dans plusieurs compagnies, mais il est parfaitement impossible de savoir quel est le pourcentage d'actions que l'on possède. En revanche, il est possible de se faire racheter, et donc de perdre au milieu du jeu sans pouvoir rien faire. La seule solution que j'ai trouvée est d'acheter ses propres actions dès qu'il y en a de disponibles.

Une antiquité à sa naissance

Graphiquement, ce n'est pas tout à fait ça. Alors qu'il est sorti plusieurs années après Transport Tycoon, la où TT proposait des animations et un scrolling de terrain fluide, A4 redessine péniblement la carte, carré par carré, à chaque recentrage, et les trains sautent de section de voie en section de voie. Alors certes, les cartes sont grandes, mais c'est très décevant.

Quel dommage!

Alors que beaucoup d'efforts et d'argent a été mis en œuvre pour la vidéo d'introduction avec James Coburn, l'on se dit que peut-être les designers auraient dû réduire leurs prétentions et sortir quelque chose qui marche. Parce que des points positifs, il y en a!

Une très grande liberté dans la pose des rails

Avec 3 niveaux en dessous du sol et 7 au dessus, l'on peut mettre les voies dans tous les sens: métros souterrains et aériens, croisements multiples, c'est un vrai plaisir.

Une ambiance particulière

L'alternance jour/nuit avec les dernières lumières dans vos gares, la musique d'ambiance pour chaque saison, et, paradoxalement, la sensation de vide qui vient du manque d'animations (les seules choses qui bougent sont vos propres véhicules), rend une atmosphère hors du temps, un peu bizarre, et très propice aux longues séances de jeu.

D'avoir proposé des scénarios basées sur de vraies villes est également un plus: l'on peut bidouiller le métro de Paris, développer New York ou Vienne.

Le nombre de bâtiments différents est également très important, et permet des villes variées. L'on pourra ainsi développer un quartier entier en s'assurant qu'il est bien desservi en transports, y installer quelques commerces, puis des bâtiments plus importants.

Une approche économique sérieuse

Quand bien même la simulation est défectueuse, l'idée de départ était bonne. Les rapports sont complets, le bilan de fin d'année est remarquable, et l'on gagne vraiment à les étudier en détail.

Face au mastodonte qu'est OpenTTD, la référence en gestion de transports, et, à mon humble avis, la référence tout court en matière de jeux libres, beaucoup de jeux font aujourd'hui pâle figure. A4 Network$ aurait pu connaître plus de succès sans ses nombreux bugs, mais il apparaît aujourd'hui bien plus dépassé que de nombreux jeux d'époque. Notons que la série des A-Train à laquelle appartient A4 continue de s'étendre, le dernier né de la série, A-Train 9, est sorti en 2010, dans la discrétion la plus absolue.

samedi 24 novembre 2012

Ces jeux qui m'ont marqué - Railroad Tycoon

L'ancêtre des jeux de train! Railroad tycoon, sorti en 1990 et qui tournait comme une bête sur mon 386 portable monochrome (eh oui, ça existait!) et tenait à l'aise sur une disquette, proposait au joueur de devenir un magnat du rail aux États-Unis, au Royaume-Uni, ou en Europe.

Le joueur peut donc installer ses rails et ses stations à travers la carte pour relier les villes et les industries et tenter d'optimiser son réseau pour maximiser les profits, en bon capitaliste qui se respecte. Au fur et à mesure du temps, de nouvelles locomotives plus performantes voient le jour, tandis que de nombreux compétiteurs installent eux-mêmes leurs réseaux.

Les contrôles sont fins, et il est possible de donner des ordres complexes à ses trains, comme par exemple de zapper les petites stations.

Le jeu est bourré d'humour, avec des titres de journaux délirants qui apparaissent régulièrement, les commentaires de fin d'année (mes actionnaires étaient apparemment "outrés" de mes résultats) ou encore la fin du jeu, où l'on obtient une certaine retraite en fonction de l'argent récolté (si l'on finit dans le noir, l'on se voit notifié une reconversion en ramoneur!).

Une autre très bonne idée est d'avoir fourni de vraies zones du monde pour jouer: il est très plaisant de créer sa ligne Paris-Lyon, ou encore de creuser sont tunnel sous la manche.

Le jeu est maintenant disponible gratuitement en abandonware, il tourne très bien sous DosBox. Une fois passé la première impression (c'est quand même assez moche), les vieilles sensations sont de retour.

mercredi 14 novembre 2012

Où l'on reparle des heuristiques

J'ai eu l'occasion récemment de me remettre aux heuristiques des fonctions Postgresql, que j'avais mentionnées dans un post précédent

Voyons le problème: l'on a un certain nombre de valeurs que l'on veut rechercher dans une grande table. On insère donc ces valeurs dans une table temporaire, et l'on fait une jointure. En fonction du nombre de valeurs dans la table temporaire, le planificateur de tâches va soit utiliser l'index, soit ordonner la table temporaire et faire un merge join. Sauf que depuis l'application, l'on prépare le plan à la première exécution, et l'on a donc aucune certitude sur le plan choisi. Pire, souvent les premières requêtes chargent un grand nombre de valeurs, et optimisent donc le plan pour ce cas, alors qu'ensuite le système se stabilise sur de toutes petites recherches, qui prennent donc beaucoup plus de temps.

create table data(i integer not null);
insert into data select generate_series(1, 1000000);
create index data_idx on data(i);

create temp table search(i integer not null);
insert into search select generate_series(1, 1000);
analyze search;
select d.i
from data d
join search s on d.i = s.i
truncate search;
insert into search select generate_series(1, 1);
analyze search;
select d.i
from data d
join search s on d.i = s.i

L'on pourrait empêcher la préparation du plan, ce qui oblige l'application à différencier entre les requêtes que l'on peut préparer et les autres. Mais il y a aussi une solution: doucement orienter le planificateur de requêtes vers le plan le plus efficace en moyenne, à l'aide d'une fonction et de hints bien choisis.

create or replace function get_search() 
returns table(i integer) 
rows 1 
volatile as
$$
 select * from search;
$$ language sql;

truncate search;
insert into search select generate_series(1, 100000);
analyze search;
select d.i
from data d
join get_search() s on d.i = s.i

L'on aura beau mettre autant de lignes que l'on veut dans la table search, le plan passera toujours par l'index de la grande table:

dimanche 4 novembre 2012

Postgresql - Arguments nommés

Je ne m'en étais même pas rendu compte: Postgresql 9.2 permet maintenant d'utiliser des paramètres nommés au sein de ses fonctions, en place des paramètres positionnels. C'est à dire qu'avant, il fallait écrire:

create function myadd(int, int) returns int as
$$
 select $1 + $2;
$$ language sql;

Mais maintenant, l'on peut écrire:

create function myadd(x int, y int) returns int as
$$
 select x + y;
$$ language sql;

Ce qui est quand même nettement plus lisible, non?

mardi 30 octobre 2012

Un coalesce en colonnes

Ça faisait longtemps que l'on avait pas fait de SQL, non? Alors, petit exercice. Soit une table contenant pour chaque date le nom d'un champ qui change de valeur, et sa nouvelle valeur. Comment transformer cette approche historique en approche par état, où l'on voit à chaque instant la valeur de chaque champ?

Tout d'abord, créons la table.

create table history(t serial, field text, value integer);
insert into history values(default, 'a', 1);
insert into history values(default, 'b', 12);
insert into history values(default, 'a', 3);
insert into history values(default, 'c', 101);
insert into history values(default, 'b', 12);
insert into history values(default, 'b', 14);
insert into history values(default, 'a', 3);
insert into history values(default, 'a', 1);
insert into history values(default, 'c', 102);
insert into history values(default, 'a', 2);
insert into history values(default, 'b', 13);

Pour visualiser la valeur de chaque champ, on donnera à chaque champ sa colonne, pour l'associer à l'ensemble des moments qui nous intéressent. Une jointure gauche s'impose donc:

select m.t, a.value as a, b.value as b, c.value as c
from history m
left outer join history a on m.t = a.t and a.field = 'a'
left outer join history b on m.t = b.t and b.field = 'b'
left outer join history c on m.t = c.t and c.field = 'c'
order by m.t

tabc
11
212
33
4101
512
614
73
81
9102
102
1113

C'est à peu près ça, sauf qu'il faut maintenant remplir les vides avec la dernière valeur disponible. Une sorte de coalesce, mais vertical.

Les sous-requêtes viennent rapidement à notre aide: pour chaque champ, l'on veut récupérer la dernière valeur en ordonnant par le temps, ce qui est facile grâce à limit. Voici donc:

select
 m.t,
 (select value from history h 
  where h.t <= m.t 
  and field = 'a' 
  and value is not null 
  order by h.t 
  desc limit 1) as a,
 (select value from history h 
  where h.t <= m.t 
  and field = 'b' 
  and value is not null 
  order by h.t desc 
  limit 1) as b,
 (select value from history h 
  where h.t <= m.t 
  and field = 'c' 
  and value is not null 
  order by h.t desc 
  limit 1) as c
from history m

tabc
11
2112
3312
4312101
5312101
6314101
7314101
8114101
9114102
10214102
11213102

Byzance! Mais arrêtons nous un instant: n'est-ce pas terriblement inefficace d'avoir des sous-requêtes dans le select? Peut-on trouver une autre solution, en utilisant par exemple des window functions? Ça vaut la peine d'essayer. Je ne crois pas qu'il existe de window function qui remplisse exactement ce rôle, mais une approche possible consiste à commencer par écrire la requête qui a un t donné donne le dernier t disponible pour le champ, en demandant à la window function d'aller chercher le max(t) pour un champ donné. La fenêtre par défaut allant jusqu'à la ligne courante, et les entrées nulles de notre jointure gauche étant par définition plus petites que le temps précédent, ça marche plutôt bien:

select 
 m.t, 
 max(a.t) over (order by m.t) as a,
 max(b.t) over (order by m.t) as b,
 max(c.t) over (order by m.t) as c
from history m
left outer join history a on m.t = a.t and a.field = 'a'
left outer join history b on m.t = b.t and b.field = 'b'
left outer join history c on m.t = c.t and c.field = 'c'
order by m.t

tabc
11
212
332
4324
5354
6364
7764
8864
9869
101069
1110119

Une fois que le temps attendu est disponible dans sa colonne, il est trivial d'obtenir la solution en joignant la table ainsi créée avec la table "history", pour chaque champ, afin d'en retrouver la valeur. La requête a pris du poids:

select h.t, a.value, b.value, c.value
from
 (select 
  m.t, 
  max(a.t) over (order by m.t) as a,
  max(b.t) over (order by m.t) as b,
  max(c.t) over (order by m.t) as c
 from history m
 left outer join history a on m.t = a.t and a.field = 'a'
 left outer join history b on m.t = b.t and b.field = 'b'
 left outer join history c on m.t = c.t and c.field = 'c'
 order by m.t) as h
left outer join history a on h.a = a.t and a.field = 'a'
left outer join history b on h.b = b.t and b.field = 'b'
left outer join history c on h.c = c.t and c.field = 'c'
order by t

Alors, est-ce plus rapide que l'approche par sous-requêtes dans le select? Malheureusement, non... Je m'attendais à une grosse différence, mais le planificateur de requête est loin d'être bête, et avec les bons index, les deux approches se valent, avec un léger avantage pour l'approche par sous-requête.

En cadeau bonus, cependant, le plan de notre dernière requête:

samedi 13 octobre 2012

Bug, partie 2

Je l'ai trouvé! Une saleté de race condition qui m'a bien pourri la vie pendant 4 jours. Nous avons en effet dans le code le concept de souscription via callbacks vers des objets, et mon code en regardait la valeur avant de souscrire: résultat, si l'état changeait entre le moment où l'on regarde la valeur, et le moment où l'on souscrit, boum, un trou, et l'on restait sur l'ancienne valeur. Il a suffi d'utiliser une autre primitive qui permettait de souscrire et de récupérer la valeur atomiquement, et tout est rentré dans l'ordre.

Cela me rappelle un autre bug qui m'avait également tenu en haleine pour un bon bout de temps: au final, c'était juste une fonction de hachage qui avait un buffer marqué par erreur comme "static", c'est à dire grosso modo global. Cela rendait la fonction non ré-entrante, et causait donc des erreurs de lookup dans les tables de hachage lors des pics de charge.

dimanche 7 octobre 2012

Bug

Je le traque depuis plusieurs jours. Au début, je n'avais aucune idée de l'animal que je poursuivais, je ne retrouvais que les dégâts causés bien après son passage, de sorte qu'il m'était impossible de l'attraper. Pendant près d'une journée, je me suis complètement fourvoyé sur des traces que je pensais être à lui, mais j'ai fini par me rendre compte que ces traces étaient normales et que c'était juste une fausse piste.

Alors j'ai posé des pièges. Je sait par où il passe, et je voulais tenter d'en apprendre plus. J'ai confectionné, puis tendu mes filets, hier vers 11h. Puis je suis parti rejoindre des collègues à Borough Market, et mangé d'excellentes pâtes au sanglier, suivies d'un bretzel (ohhh...) et d'un morceau de tarte au chocolat (ahhhh!). Je suis revenu, et j'ai regardé si j'avais été chanceux.

Je ne trouvais que quelques crottes insignifiantes, l'animal était bien passé par là mais n'avait rien laissé d'utile. Je vaquais donc à d'autres occupations, et vérifiais vers 16h. Bingo! Non seulement il était passé, mais il m'avais laissé une magnifique trace, que je m'empressais d'étudier.

Hier soir, j'en savais beaucoup plus sur l'animal. Pas suffisamment pour savoir exactement ce que c'était, et donc m'en débarrasser, mais assez pour sérieusement raffiner mes pièges. J'ai bon espoir de l'attraper Lundi.

jeudi 27 septembre 2012

Placement new

J'ai enfin trouvé une utilité à placement new aujourd'hui. Pour rappel, il s'agit de construire un objet (en gros, d'appeler son constructeur) dans un emplacement mémoire déjà alloué.

De fait, tout est venu de la propension du vecteur à initialiser tout ce qu'on lui donne. C'est généralement fort pratique, mais parfois, ce n'est pas le comportement attendu. Par exemple, l'implémentation d'une table de hachage en adressage ouvert, où toutes les valeurs sont placées de manière contigüe en mémoire, nécessite l'allocation d'un bloc, puis la construction (et la destruction) de chaque cellule au fur et à mesure du remplissage de la table.

C'est donc exactement l'approche que j'ai prise: faire un

char * mem = new char[size * sizeof(T)])
pour allouer suffisamment de mémoire pour y placer size objets T. La mémoire n'est pas initialisée, il n'y a donc pas de pénalité à payer en termes de performances. Ensuite,
new (mem + index * sizeof(T)) T
pour construire l'objet au moment où j'en ai besoin. À la destruction, il faut manuellement appeler le destructeur:
reinterpret_cast<T *>(mem + index * sizeof(T))->~T();

lundi 3 septembre 2012

Et pourtant c'était là...

Comment faites-vous pour vérifier si un élément appartient à un std::set? Moi, je faisais set.find(e) != set.end(). Eh ben, il y a encore plus simple: set.count(e). Ça a toujours été là, et je ne m'en étais même pas rendu compte...

dimanche 26 août 2012

Oeuf de canard - Un graphe de scène en OCaml

Je ressors une vieillerie, légèrement dépoussiérée: j'avais posté il y a bien longtemps une vidéo d'une planète terre tournoyant, mais je n'avais pas partagé le code. C'est maintenant fait, et mon graphe de scènes en OCaml est maintenant dans Git. C'est assez trivial, si ce n'est quelques difficultés au niveau du calcul du frustum et de l'héritage en OCaml (j'utilise les fonctionnalités objet).

C'est suffisant pour s'amuser, même si un parseur dans un format 3D standard ne serait pas de trop (j'avais commencé avec les fichiers Wavefront .obj). OpenGl a cependant beaucoup évolué depuis, donc c'est très old school.

jeudi 23 août 2012

Postgresql - Méthodes de classes

J'ai lu avec intérêt aujourd'hui un article décrivant Postgres en tant que système de base de données relationnelle-objet. Était évoquée notamment une fonctionnalité que je ne connaissais pas: une fonction sur une table peut être appelée directement sur la table via un '.' au lieu d'avoir à utiliser l'appel usuel avec parenthèses. Démonstration:

create table resultats(
 equipe text not null,
 victoires integer not null,
 nuls integer not null,
 defaites integer not null);

insert into resultats values('Troyes', 3, 0, 1);
insert into resultats values('Sète', 1, 2, 0);

create function total(resultats) returns integer as
$$
 select $1.victoires + $1.nuls + $1.defaites;
$$ language sql;

create function points(resultats) returns integer as
$$
 select $1.victoires * 3 + $1.nuls * 2 + $1.defaites;
$$ language sql;

Si l'on veut visualiser le nombre total de matchs et les points de chaque équipe, l'on peut utiliser la requête suivante:

select r.equipe, total(r), points(r) from resultats r;

Mais il est également possible de l'écrire comme ça:

select r.equipe, r.total, r.points from resultats r;

En dehors de l'aspect purement syntactique de la chose, c'est aussi une manière de considérer les fonctions total et points comme étant des méthodes sur la classe resultats.

vendredi 17 août 2012

La bonne référence du C++

Je suis passé par la doc de SGI, la doc de gcc, cplusplus.com, mais depuis peu l'on voit fleurir en haut des résultats des moteurs de recherche le site cppreference.com, et je crois qu'il contient tout ce qu'il faut pour mon bonheur:

  • Une description détaillée de la bibliothèque C++
  • Avec la description systématique de tous les paramètres et de tous les types de retour
  • Avec la description systématique de la complexité algorithmique de chaque méthode ou fonction lorsqu'elle est définie
  • Référençant les changements induits par C++11
  • Bourré d'hyper liens, lisible, joli, et sans pub
Alors oui, c'est en anglais, la partie française du site est très incomplète mais n'attend que votre talent de traducteur!

Explicit virtual overrides

class A
{
  virtual void f() const;
};

class B
{
  void f();
};

Regardons le code d'un peu plus près. Le "const" manquant dans la deuxième classe, est-ce voulu, ou est-ce une erreur du programmeur? La fonctionnalité C++11 "explicit virtual overrides" ajoute des mot-clés au langage pour permettre de spécifier très précisément le type d'héritage désiré. Démarrons sans plus attendre.

override

Le mot-clé override indique que la méthode doit absolument hériter, le compilateur jettera donc une erreur si ce n'est pas le cas. Par exemple:

class A
{
  virtual void f();
};

class B : public A
{
  virtual void f() override;
};

class C : public A
{
  virtual void f() const override;
};

La déclaration de f dans la classe C provoque l'erreur suivante: error: ‘virtual void C::f() const’ marked override, but does not override. Facile!

final

Le mot-clé final, bien connu des javaistes et attendu depuis longtemps du côté C++, peut s'appliquer soit à une classe, soit à une méthode. Dans le premier cas, le compilateur interdira d'hériter de la classe, alors que dans le second, il interdira l'héritage de la méthode.

class A
{
  virtual void f() final;
};

class B final : public A
{
  virtual void g();
};

class C : public A
{
  virtual void f() override;
};

class D : public B
{
};

La définition de la méthode f dans la classe C provoque le message d'erreur suivant: error: overriding final function ‘virtual void A::f()’, indiquant que la classe f a été marquée finale dans une classe parente. La définition de la classe D provoque l'erreur suivante: error: cannot derive from ‘final’ base ‘B’ in derived type ‘D’, indiquant que la classe D tente de dériver d'une classe finale.

Voilà des fonctionnalités qui vont, je l'espère, d'éviter beaucoup de maux de tête. En indiquant clairement au niveau de la classe parente quelles méthodes ne peuvent être héritées, et en indiquant clairement dans la classe fille les méthodes qui viennent d'une classe de base, l'on documente le comportement attendu de ses objets en laissant le compilateur en faire la vérification.

Cependant, l'exercice a ses limites: contrairement au const qui a un côté viral en forçant tous les types associés à être const-corrects, les mot-clés d'héritage peuvent être utilisés autant ou aussi peu que le programmeur le souhaite. Il faudra donc s'assurer qu'au niveau d'un projet, tout le monde suit le même standard, sinon le remède risque d'être pire que le mal: un programmeur pourra être persuadé que le code est faux car il manque un override, alors que l'auteur avait simplement décidé de l'ignorer.

mardi 14 août 2012

Extended friend declarations - Je zappe

J'ai lu et relu la spec, fait quelques tests, et je ne vois toujours pas vraiment ce que cette fonctionnalité ajoute au standard. De ce que j'en comprends, les déclarations de friend sont plus génériques et plus cohérentes, mais c'est à peu près tout... J'ai essayé quelques exemples pour trouver quelque chose qui ne marchait pas avant et qui marche maintenant, mais je n'ai rien. Si quelqu'un a un test, je suis preneur!

lundi 6 août 2012

User-defined literals

Plongeons immédiatement dans une fonctionnalité nettement plus discutable de C++11, les "User-defined literals". Vous souvenez-vous du petit "f" à la fin des nombres pour indiquer qu'il s'agit d'un flottant? Eh bien, ceci est la généralisation du concept. L'idée est de fournir la possibilité de créer des opérateurs qui travaillent sur certains types de base pour retourner un type défini par l'utilisateur, via un suffixe. Démarrons tout de suite avec un exemple: définissons un nouveau suffixe _str qui transforme une chaine vers un std::string.

#include <string>
#include <iostream>

std::string operator "" _str(const char * str, size_t length)
{
  return std::string(str, length);
}

int main()
{
  std::cout << "abc"_str + "def"_str << std::endl;
  return 0;
}

En définissant l'opérateur "" suivi du suffixe choisi, l'on peut maintenant transformer nos chaînes en std::string directement, nous permettant d'utiliser un + qui aurait sinon fait hurler le compilo. Notons qu'il est possible de faire à peu près n'importe quoi dans l'opérateur "", comme de bidouiller la chaîne, ou d'en retourner la longueur, ou quoi que ce soit d'autre. Le type de retour est librement défini par l'utilisateur.

La liste des types que peut prendre l'opérateur "" en paramètre est la suivante:

  • char const *, pour les nombres sous forme de chaîne
  • unsigned long long int, pour les entiers
  • long double, pour les flottants
  • char const *, size_t, pour les chaînes
  • wchar_t const *, size_t, pour les chaînes de caractère wide
  • wchar16_t const *, size_t, pour les chaînes de caractères 16 bits
  • wchar32_t const *, size_t, pour les chaînes de caractères 32 bits
Petites explications supplémentaires sur le premier élément: il s'agit de lire un nombre directement sous forme de chaîne, le compilateur s'abstenant alors d'en faire la transformation préalable vers un entier. Cet opérateur peut être utile si l'on veut lire de très longs entiers, par exemple dans une bibliothèque de calcul en précision infinie. Voyez l'exemple suivant:

#include <string>
#include <iostream>
#include <sstream>

std::string operator "" _miaou(const char * str)
{
  return("***" + std::string(str) + "***");
}

std::string operator "" _ouaf(unsigned long long int i)
{
  std::ostringstream str;
  str << i;
  return "***" + str.str() + "***";
}

int main()
{
  std::cout << 1234567890123456789012345678901234567890_miaou << std::endl;
  std::cout << 1234567890123456789012345678901234567890_ouaf << std::endl;  

  return 0;
}

À l'exécution, s'affiche:

***1234567890123456789012345678901234567890***
***12446928571455179474***

Les plus observateurs d'entre vous auront remarqué que les opérateurs ne prennent pas d'entiers négatifs. En effet, -123_miaou est interprété comme operator-(operator "" _miaou(123)). Il faudra donc redéfinir operator- si vous voulez retourner un type bizarre.

J'espère sincèrement que cette fonctionnalité va rester confidentielle, car elle me semble un peu trop facile à abuser. Cependant, je ne demande qu'à être convaincu: une bibliothèque physique permettant d'utiliser les unités SI en suffixe pourrait peut-être permettre d'écrire du code fortement typé qui reste très lisible. À voir.

jeudi 2 août 2012

Delegating constructors

Pas vraiment la fonctionnalité de C++11 qui fera rêver dans les chaumières, mais tout à fait utile dans certains cas, la délégation de constructeurs permet tout simplement d'appeler un constructeur depuis un autre constructeur. Voyons immédiatement un exemple:

#include <string>
#include <iostream>

class A
{
public:
  A():
    _a("abc"),
    _b(5),
    _c(0.4)
  {
    std::cout << "Constructeur par défaut" << std::endl;
  }
  
  A(const std::string & a, double c):
    A()
  {
    _a = a;
    _c = c;
    std::cout << "Deux paramètres" << std::endl;
  }
  
  A(const std::string & message):
    A(message, 0.3)
  {
    std::cout << "Un paramètre" << std::endl;
  }
  
  void print()
  {
    std::cout << _a << ", " << _b << ", " << _c << std::endl;
  }

private:
  std::string _a;
  int _b;
  double _c;
};

int main()
{
  std::cout << "---" << std::endl;
  A a1;
  a1.print();

  std::cout << "---" << std::endl;
  A a2("def", 0.7);
  a2.print();

  std::cout << "---" << std::endl;
  A a3("ghi");
  a3.print();
}

Le programme affiche

---
Constructeur par défaut
abc, 5, 0.4
---
Constructeur par défaut
Deux paramètres
def, 5, 0.7
---
Constructeur par défaut
Deux paramètres
Un paramètre
ghi, 5, 0.3

Il est aisé de suivre les constructeurs pour trouver ceux qui ont été appelés. Notons cependant une petite restriction: lorsque l'on délègue un constructeur, il n'est pas possible d'avoir quoi ce ce soit d'autre dans la liste d'initialisation, que ce soient des appels de classe parente ou de l'initialisation de membres. C'est pour cela que dans l'exemple, dans le second constructeur, _a et _c reçoivent l'affectation dans le corps du constructeur.

Il est plus généralement raisonnable d'avoir un constructeur générique prenant le plus grand nombre de paramètres, et des constructeurs spécifiques qui délèguent au constructeur générique, afin de permettre par exemple certaines restrictions sur les paramètres.

mardi 24 juillet 2012

Template aliases

Les "typedef de templates" est une fonctionnalité qui est souvent demandée, et C++11 la fournit! Plutôt que le mot-clé "typedef", c'est une syntaxe à base du mot-clé "using" qui est utilisée, mais l'idée est la même:

template<typename T1, typename T2, typename T3>
class A
{
};

template<typename T1, typename T3>
using A_int = A<T1, int, T3>;

template<typename T>
using A_same = A<T, T, T>;

Notons que la même fonctionnalité était disponible en C++03 en passant par une classe intermédiaire:

template<typename T1, typename T2, typename T3>
class A
{
};

template<typename T1, typename T3>
class A_int
{
  typedef A<T1, int, T3> TypeT;
};

template<typename T>
class A_same
{
  typedef A<T, T, T> TypeT;
};

L'on pouvait utiliser alors A_int::TypeT et A_same::TypeT, mais c'est quand même plus joli lorsque l'on peut écrire ses "using" directement. À noter que la syntaxe "using" permet également de renommer des types non templatés, exactement comme le typedef:

using MyInt = int;

Surtout du sucre syntaxique, donc, mais du sucre qui devrait rendre le code plus clair en évitant la multiplication de classes triviales ne servant qu'à renommer ses types.

jeudi 19 juillet 2012

Non-static data member initializers

Voici un petit changement dans C++11 qui semble tout droit venir de Java. L'idée est de permettre l'initialisation des membres non-statiques au niveau de leur déclaration. Voyons tout de suite ce à quoi cela peut ressembler:

#include <string>
#include <iostream>

class A
{
public:
  int a = 3;
  std::string b = "abc";
};

int main()
{
  A a;
  std::cout << a.a << ", " << a.b << std::endl;

  return 0;
}

Compilons (remarquons au passage que si l'on ne spécifie pas l'option "--std=c++11", l'on a un petit warning du type "init.cpp:7:11: warning: non-static data member initializers only available with -std=c++11 or -std=gnu++11 [enabled by default]"). Comme prévu, le programme affiche "3, abc".

Remarquons également qu'il est possible d'utiliser des fonctions libres, ou encore des méthodes, pour initialiser ses variables, et qu'il est également possible d'utiliser les listes d'initialisation, soit avec le "=" réglementaire, soit en accolant les {} directement après la variable.

class A
{
public:
  int a = 3;
  std::string b = thestring();
  std::vector<int> c = {1, 2, 3, 4, 5};
  std::vector<double> d{1, 2, 3, 4, 5};

  std::string thestring()
  {
    return "abc";
  }
};

Il est possible de forcer la valeur dans un constructeur, ce qui permet d'implémenter une valeur par défaut: dans l'exemple suivant, _batchSize vaut soit 1000 pour le constructeur par défaut, soit la valeur passée en paramètre pour l'autre constructeur.

class A
{
public:
  A()
  {
  }
  
  A(int batchSize):
  _batchSize(batchSize)
  {
  }
  
  int _batchSize = 1000;
};

Alors, utile? Certainement. Un des gros avantages de ce type de notation est de réduire la duplication de code lorsqu'une classe a beaucoup de constructeurs qui initialisent tous certaines variables à la même valeur, réduisant le risque d'erreurs (pensez à une variable "isRunning" par exemple). En revanche, et c'est mon souci principal, cela augmente le nombre d'endroits où les initialisations sont effectuées, rendant le code plus difficile à lire: il n'est pas suffisant de regarder son constructeur, il faut également aller dans le fichier d'en-tête pour voir à quoi seront initialisées les autres valeurs. Pour cette raison, il sera probablement sage de limiter cette fonctionnalité aux classes relativement simples, ou à certains membres bien définis (typiquement, les variables changent peu, liées à l'état de la classe, comme les batchSize, isRunning, objectName...). Mais dans des classes très complexes, c'est risquer de rendre le code bien moins compréhensible.

mardi 17 juillet 2012

Bientôt un client Steam sous Linux

Cela vient d'être confirmé par Valve, via une entrée sur un tout nouveau blog: une équipe a été formée pour créer et maintenir un client Steam et pour porter des jeux nativement sous Linux.

L'équipe Valve Linux se concentre sur le moment sur Ubuntu (12.04), mais le support d'autres distributions est prévu.

L'annonce indique que pour l'instant, l'équipe a porté Steam avec la plus grande partie de ses fonctionnalités, ainsi qu'une version non optimisée de Left For Dead 2. Le travail va donc consister d'une part à passer Steam en bêta-test, et d'autre part à continer le travail d'optimisation sur leur moteur. L'on peut espérer que d'icic à un certain temps, la plupart des nouveaux jeux Valve aient un port Linux. Ajoutés aux "humble bundles" qui sont pour la plupart jouables en natif, c'est une ludothèque limitée, mais tout à fait intéressante, qui pourrait voir le jour. À terme, d'autres éditeurs pourraient s'engouffrer dans la brêche. La fin du monopole de Windows sur les jeux?

Et quid de la rumeur persistante qui dit que Linux est la première étape vers une console de salon estampillée Valve?

dimanche 15 juillet 2012

g++ 4.7 dans Wheezy

Bonne surprise lors de ma mise à jour de ce week-end, g++4.7 est arrivé dans Debian Wheezy. C'est l'occasion de se pencher sur les nouvelles fonctionnalités de c++11 intégrées dans cette version. D'après la page récapitulative, les nouveautés sont les suivantes:

  • Non-static data member initializers
  • Template aliases
  • Delegating constructors
  • User-defined literals
  • Extended friend declarations
  • Explicit virtual overrides

L'on sent que l'on arrive aux tréfonds du standard: beaucoup de ces fonctionnalités sont à la limite du sucre syntactique ou renforcent la sécurité du langage, mais il n'y a pas de révolutions.

Si l'on regarde du côté de ce qui manque encore, l'on trouve surtout des fonctionnalités de parallélisation, qui ne sont probablement pas la priorité de l'équipe gcc.

Néanmoins, nous reverrons en détail ces nouvelles fonctionnalités dans les posts qui viennent.

jeudi 12 juillet 2012

Ces jeux qui m'ont marqué - Settlers I et II

Et un saut dans la modernité, avec ces jeux apparus sur PC respectivement en 1994 et en 1996. Suffisamment similaires pour que j'aie du mal à les distinguer après toutes ces années, je parlerai surtout du deuxième opus, mais les fondements sont les mêmes.

Settlers est un jeu de stratégie qui vous met à la tête d'un royaume qu'il faut faire prospérer en construisant une économie efficace. Au fur et à mesure de son extension, l'on rencontrera des royaumes ennemis qu'il faudra abattre.

Jusque là, rien de très original, mais c'est dans la complexité du système économique que Settlers I et surtout le II fait toute la différence. En effet, il faut construire divers bâtiments pour construire des circuits de production permettant d'alimenter son expansion. Par exemple, la ferme produit du blé, qu'il faut envoyer au moulin pour faire du pain, lequel servira à nourrir les mineurs, lesquels génèrent du fer et du charbon, lesquels sont utilisés pour construire des épées et des boucliers, lesquels servent à équiper ses soldats. Il faut produire de la pierre et du bois pour construire de nouveaux bâtiments, de l'or et de la bière pour renforcer ses soldats, et ainsi de suite.

En plus de construire des bâtiments, il faut les relier par des routes jalonnées de drapeaux. Un passeur prendra place entre deux drapeaux, et passera les ressources d'un drapeau à un autre. Il faut donc faire très attention à la manière dont on positionne ses bâtiments pour s'assurer que les marchandises sont transférées le plus rapidement possible, sans causer de bouchons.

Ce qui caractérise Settlers en comparaison des autres ténors du genre est son rythme beaucoup plus lent: puisqu'il faut attendre patiemment que chaque ressource fasse sont chemin à travers chaque circuit, l'on a beaucoup plus de temps pour placer judicieusement les prochains bâtiments, ou retravailler une route trop encombrée. Mais attention à ne pas se laisser hypnotiser, car il est facile de passer des heures à contempler ses petits géologues sauter de joie quand ils trouvent un filon, ses bouchers découper les cochons en tranches, ses passeurs faire des boules de chewing-gum quand ils n'ont rien à faire... Les animations très soignées rendent le jeu absolument magnifique à contempler.

Stratégiquement, c'est également tout à fait intéressant: l'on peut soit partir à la conquête du plus grand nombre de territoires dès le début, et essayer d'étrangler son adversaire, ou le coincer petit à petit, ou encore tenter de conquérir ses centres névralgiques en coupant son terrain en morceaux. Dans les niveaux les plus avancés, l'AI ne se défend pas mal du tout, donnant un challenge tout à fait intéressant.

Les derniers Settlers (de 3 à 7 ou 8) ont perdu, je trouve, ce côté nonchalant et mignon des deux premiers épisodes, pour se rapprocher des standards du jeu de stratégie en temps réel. Il demeurent intéressants, mais pour moi, pas de Settlers sans petits drapeaux!

vendredi 6 juillet 2012

Ces jeux qui m'ont marqué - Wizball

Encore une antiquité avec Wizball, jeu d'arcade paru en 1987. Le joueur incarne un magicien transformé en une boule verte, qui doit redonner leurs couleurs à une série de niveaux. Au début, le joueur n'a qu'un contrôle très limité de sa boule, mais au fur et à mesure qu'il détruit les mines ennemies et collecte des bulles, il va pouvoir améliorer sa maniabilité, puis appeler son chat (une autre boule verte plus petite, qu'il peut contrôler partiellement), puis obtenir des pouvoirs plus destructeurs.

Régulièrement, des boules de couleur traversent l'écran. En tirant dessus, elles tombent en gouttes, qui peuvent être récoltées par le chat (mais attention aux gouttes blanches, qui rendent le chat saoul, et aux gouttes noires, qui "éteignent la lumière"). Une fois suffisamment de couleurs récupérées, l'on part pour le laboratoire où le magicien mélange les couleurs, et colore une partie du niveau. Au bout de trois colorations, le niveau est complet, et un nouveau niveau s'ouvre.

Le jeu est plutôt ardu, les décors complètement gothiques au début deviennent franchement psychédéliques quand ils ont été colorés. Un gros point fort est la possibilité de jouer à deux en coopératif, l'un des joueurs prenant le rôle du chat.

Enfin, Wizball, c'est la musique. Chaque plateforme, C64, Atari et Amiga, maintiennent avoir la meilleure musique. Moi, c'était l'Atari, et cette musique, elle ne m'a plus lâché!

mardi 26 juin 2012

Ces jeux qui m'ont marqué - Midwinter

Midwinter (captures d'écran) est un jeu d'action / rôle à la première personne paru en 1989, où l'on incarne le chef de la milice d'une petite île enneigée qui vient d'être envahie par de belliqueux voisins. À skis, en moto-neige ou en planeur, il faut parcourir l'île et organiser sa défense en recrutant les villageois pour saboter l'offensive et détruire la force adverse.

C'était la première fois que j'ai autant ressenti l'effet "bac à sable". Une fois le jeu démarré, c'est une immense étendue gelée que l'on peut parcourir à loisir dans différents types de véhicules, pour passer par le village de son choix et y récupérer ce que l'on peut, en munitions, dynamite, ou autres véhicules. L'aspect recrutement était également novateur, puisque l'on pouvait convaincre d'autres villageois aux compétences diverses de se joindre à nous. Il fallait bien s'entendre pour recruter, et il fallait donc parfois passer par un ami commun afin de récupérer un personnage particulièrement doué dans un domaine dont on avait besoin. Une fois un personnage recruté, l'on peut le contrôler à son tour, et se retrouver au final à la tête d'une grosse équipe, prenant le rôle de chacun sur un intervalle de 2 heures.

La conduite des véhicules n'était pas évidente: il était bien difficile de ne pas se planter périodiquement avec une moto-neige en passant par un terrain difficile, et une fois blessé et avec sa moto-neige cassée, rejoindre le village le plus proche n'était pas une sinécure. Quant aux planeurs, c'est bien simple, je n'ai jamais réussi à les utiliser, et je me plantais systématiquement.

Le paysage en polygones ombrés semblerait aujourd'hui laid à faire peur, mais c'était une sacrée avancée à l'époque, et les différents types de terrains comme les villages ou les unités étaient tout à fait reconnaissables.

Les phases d'action étaient tout à fait intéressantes également: fuite en moto-neige lorsque l'on se trouve nez à nez avec l'ennemi, ou séances mémorables de snipe depuis un clocher.

Comme dans beaucoup de ces jeux qui m'ont suffisamment marqué pour que j'en cause 15 ans après, c'était d'abord une histoire d'atmosphère. L'ivresse de la liberté sur sa moto-neige, la joie d'avoir recruté un villageois utile ou d'avoir descendu une division entière au fusil, la rage de devoir se fader une grande montée à skis pour rejoindre ce village, et la victoire! Un des rares jeux de l'époque que j'avais vraiment l'impression d'avoir complété.

dimanche 20 mai 2012

Fonctions postgresql, heuristiques

Reprenons l'exemple d'hier, mais cette fois-ci avec 2 tables de 10M lignes chacune, et une requête un tout petit peu plus rigolote, puisqu'elle limite la jointure aux 10 premières lignes de la première table. Pas de surprises, le planificateur de requêtes voit que la limite permet d'aller chercher les éléments de la deuxième table via leur index, et propose une requête efficace, avec un temps d'exécution de 12 ms.

create table data1(a integer not null);
insert into data1 select generate_series(1, 10000000);
create index data1_idx on data1(a);
create table data2(a integer not null);
insert into data2 select generate_series(1, 10000000);
create index data2_idx on data2(a);

select *
from (select * from data1 limit 10) d1
join data2 d2 on d1.a = d2.a

Soyons un peu plus pervers, et prenons maintenant la limite depuis une autre table, empêchant ainsi le planificateur de voir que d1 peut être en grande partie ignorée. Ouch! Le plan en prend un coup, et le temps d'exécution explose à presque 4 secondes.

create table data_limit(l integer not null);
insert into data_limit values(10);

select *
from (select * from data1 limit (select * from data_limit)) d1
join data2 d2 on d1.a = d2.a

Si l'on sait que notre limite va être la plupart du temps petite, comment peut-on aider notre planificateur à choisir la bonne requête? Une possibilité est de passer par une fonction, et de lui passer le "hint" qui va bien, c'est à dire en indiquant le nombre de lignes auxquelles l'on s'attend via le mot-clé "rows" (notez que par défaut, Postgres suppose que la fonction renvoie 1000 lignes). Et tout d'un coup, le plan redevient raisonnable, et la requête rapide:

create function get_limited_data1()
returns table(a integer)
rows 10
as
$$
 select * from data1 limit (select * from data_limit);
$$ language sql;

select *
from get_limited_data1() d1
join data2 d2 on d1.a = d2.a

Il peut être utile de passer par une fonction pour ajuster les heuristiques du planificateur de requêtes.

Attention cependant! Si la fonction est inlinée, le planificateur perdra ses heuristiques, et pourra revenir à un plan moins efficace:

create function get_limited_data1()
returns table(a integer)
stable
rows 10
as
$$
 select * from data1 limit (select * from data_limit);
$$ language sql;

select *
from get_limited_data1() d1
join data2 d2 on d1.a = d2.a

Forcer la volatilité la plus haute permet d'empêcher l'inline de la fonction, et donc de forcer les heuristiques du planificateur.

Fonctions postgresql, inlining

Regardons d'un peu plus près les fonctions Postgres et leurs conséquences sur les performances. Créons deux tables d'entiers munies chacune d'un index, l'une faisant 2M de lignes, et l'autre seulement 10.

create table data1(a integer not null);
insert into data1 select generate_series(1, 10000000, 5);
create index data1_idx on data1(a);
create table data2(a integer not null);
insert into data2 select generate_series(1, 10);
create index data2_idx on data2(a);

Joignons les 2 tables, et oh, miracle, le planificateur de requête décide d'utiliser l'index sur la grande table. Temps d'exécution, 12 ms.

select *
from data1 d1 join data2 d2 on d1.a = d2.a;

Maintenant, utilisons plutôt des fonctions, qui retournent le contenu de la table.

create function get_data1() 
returns table(a integer)
as
$$
 select * from data1;
$$ language sql;

create function get_data2()
returns table(a integer)
as
$$
 select * from data2;
$$ language sql;

select *
from get_data1() d1 join get_data2() d2 on d1.a = d2.a;

Le plan est beaucoup moins sympathique: le planificateur ayant perdu toute information sur les index disponibles ne peut que faire une jointure bourrine, pour une exécution qui dure 2.3 secondes. Comment arranger cela?

Une fonction Postgres possède un certain nombre d'attributs qu'il est possible d'ajuster pour donner plus d'information au planificateur de tâches. L'un de ces attributs est la volatilité de la fonction. Par défaut, toute fonction est volatile, ce qui empêche le planificateur de faire aucune optimisation, mais une fonction ne contenant que des "select" peut être passé à "stable", ce qui permet au planificateur de l'inliner. Le résultat est sans appel:

create function get_data1() 
returns table(a integer)
stable
as
$$
 select * from data1;
$$ language sql;

create function get_data2()
returns table(a integer)
stable
as
$$
 select * from data2;
$$ language sql;

select *
from get_data1() d1 join get_data2() d2 on d1.a = d2.a;

L'on est revenu au premier plan, car le planificateur a tout inliné et a donc pu utiliser sa connaissance des index pour revenir au temps d'exécution initial.

Il est la plupart du temps extrêmement utile d'attribuer la bonne volatilité à une fonction afin de permettre au planificateur de requête de l'optimiser au mieux.

mercredi 9 mai 2012

Mon nouveau téléphone

C'est avec une petite larme au coin de l’œil que j'abandonne mon Nokia 6500c. C'était une brave bête, stylée, indestructible, et plutôt ergonomique, bien qu'un poil buggée. Je ne saute qu'à moitié vers le smartphone, puisque je suis maintenant l'heureux utilisateur d'un C3 "Touch & Type", qui comme son nom l'indique est muni à la fois d'un clavier et d'un écran tactile.

Au final, et après un petit peu de personnalisation, je retrouve toutes mes marques.

Cependant, y transférer mes SMS n'a pas été de tout repos. Laissez moi vous conter un peu tout ça.

Tout d'abord, je décide d'utiliser l'option Bluetooth. Il me faut m'y reprendre 3 ou 4 fois avant de comprendre comment configurer chaque téléphone pour qu'ils acceptent de se causer, mais ça fini par passer. Mais ce n'était que partie remise, puisqu'au milieu de la (longue) transmission, il plante. Je réessaye, il plante encore.

Prochaine étape: passer par le PC. Certes, mais où est la suite Nokia pour Linux? Nulle part, on dirait. Malgré les demandes répétées de quelques barbus irréductibles, Nokia ne fournit qu'une solution Windows, et les quelques applications OpenSource qui prétendent y arriver refusent obstinément de découvrir mon téléphone.

Ça fait mal, mais bon, rebootons, et installons l'OVI suite. Ah, et puis les drivers Nokia qui doivent être téléchargés depuis Windows Update. Ah, et puis il faut rebooter la machine. Ah, et puis ne faut pas brancher le téléphone trop tôt, sinon l'application est perdue. Au moment où je m'apprêtais à sacrifier un poulet, ça se connecte enfin. Youpie! Je synchronise, et au bout d'un autre (long) moment, mes contacts, mes photos et mes SMS sont sur la machine. J'y branche alors mon nouveau téléphone, et je synchronise. Malheureusement, c'est là que l'on ne s'est pas bien compris, avec la machine: elle a juste téléchargé les moitié de messages qui étaient déjà sur le téléphone, conséquence de la copie Bluetooth partielle, et m'a pourri ma boite en passant la moitié des messages à la date d'aujourd'hui.

J'ai bien ensuite perdu 2 heures à tenter de nettoyer pour resynchroniser. Il devait se souvenir de la date du dernier message téléchargé, parce qu'il ne voulait plus rien savoir et ne transmettait plus aucune donnée. Finalement, j'ai découvert qu'il était possible de faire du "glisser-déposer" pour forcer la transmission des messages vers la machine, puis de recommencer pour renvoyer vers le nouveau téléphone... Mais ce mode prend encore plus de temps, au rythme de 2 ou 3 messages à la seconde.

Finalement, tout y est. Les messages multimédia apparaissent au début au lieu de venir dans l'ordre alphabétique, mais je survivrai. Pour moi, c'est la conséquence d'applications trop intelligentes: je n'arrive pas à comprendre à l'avance les opérations que le système va effectuer, et quand je me plante, je dois alors me battre pour retrouver les fonctionnalités de base (copie ça ici) qui me permettent de réparer... Voyons cependant le bon côté des choses: l'OVI suite permet d'exporter ses SMS au format CSV. J'ai l'intention de tout coller dans une base de données, et de tirer quelques statistiques d'une inutilité crasse. Vous aurez été prévenus!

mardi 24 avril 2012

Warning gcc

Un warning bizarre de gcc. En cherchant sur la mailing list, il existe un bug similaire qui a été résolu par le passé, mais on dirait que celui là courrait encore dans gcc 4.4 (corrigé en revanche dans 4.6). Ce cas assez tordu cause un warning se plaignant que certains chemins n'ont pas de return:

// test.cpp
// Compile with:
// g++ -c test.cpp -O0 -o test -W -Wall

// Compiling with -O3 fixes the warning

#include 
#include 
 
int a()
{
  std::string str; // Removing this line fixes the warning
  int state = 1;
  if(state != 1 && state != 2) // Only having 1 condition fixes the warning
  {
    throw std::runtime_error("abc");
    //return 0; // Replacing the throw by return 0 fixes the warning
  }
  else
  {
    return 0;
  }
}

mardi 10 avril 2012

Skyrim

La voilà, la raison de mon manque d'enthousiasme à poster ces derniers temps. Avec un jeu aussi énorme dans mes pattes, mes vacances y passent à la vélocité d'un dragon souffrant d'aérophagie.

Ce n'est cependant pas une revue standard à laquelle je vais me livrer, puisque Internet vous a déjà dit que ce jeu était excellent, mais à une petite revue de certains mécanismes de gameplay que je trouve particulièrement bien pensés.

L'alchimie est bien plus amusante: au lieu de simplement attendre de monter en grade pour découvrir quelles sont les propriétés de tel ou tel ingrédient, il faut les manger! À haut niveau, l'on pourra découvrir plus de propriétés à chaque dégustation, mais puisque les propriétés sont également dévoilées lors d'un mélange réussi, l'on étend ses connaissances au fur et à mesure de nos expérimentations. L'on notera également l'amélioration de l'interface: alors que dans Oblivion, il fallait revoir chaque élément pour trouver des combinaisons, Skyrim liste clairement les potions pour lesquels on a les ingrédients disponibles.

Notons également la possibilité de forger ses armes et armures, et d'améliorer ses créations ou l'existant. Il est particulièrement agréable de porter une armure que l'on a peaufiné soi-même.

La création des objets enchantés est intéressante: l'on apprend des enchantements en détruisant des objets enchantés, pour ensuite en ré-enchanter d'autres. Pour chaque objet, il y a donc un choix à faire: le vendre pour en retirer des sous, le détruire pour ré-enchanter un autre objet plus pratique (comme un casque ou une armure) mais avec le risque que l'effet soit moins puissant à bas niveau, ou l'utiliser tel quel.

Le système de quêtes est toujours aussi bien pensé: les quêtes mineures dirigent le joueur vers les emplacement où les quêtes majeures sont disponibles: ainsi, l'alchimiste vous demandera d'aller prendre des nouvelles auprès du centurion, lequel débloque la longue série des quêtes de l'empire. La responsable de la scierie perdue au milieu des montagnes vous demander d'aller porter un mot à la ville proche, vous donnant une raison d'aller y jeter un œil.

L'on regrettera cependant l'interface utilisateur qui sent la console de jeux. Le concept d'équipements favoris est pratique, mais j'aurais aimé la possibilité d'utiliser les raccourcis claviers pour aller directement chercher les armes. Il faut également parfois de battre un peu avec la souris et le clavier lors des dialogues, les zones cliquables étant parfois mal fichues.

J'y retourne!

jeudi 22 mars 2012

Je me suis mis à Python

Mais j'ai du m'arrêter après 3 lignes, parce que je me suis fait router sur une autre tâche. Ceci dit, en 3 lignes, j'ai découvert:

  • Qu'il existe un mode Emacs tout à fait remarquable, permettant notamment d'exécuter tout ou partie du buffer, un peu comme le mode Emacs d'OCaml

  • Qu'il existe une bibliothèque pour lire les fichiers CSV, simple et complète

  • Qu'il est trivial d'exécuter des commandes shell depuis le programme

  • Mais qu'il va quand même falloir que je m'habitue à ne rien avoir à déclarer

Bon début, donc.

jeudi 15 mars 2012

DB2 ou Sybase?

C'est le choix cornélien qui me tombe dessus au boulot.

D'un côté, DB2, que nous utilisons depuis fort longtemps, mais dont les performances sont plutôt mauvaises (rien à voir à priori avec le moteur en lui même, c'est plutôt une histoire de performances du stockage réseau), à tel point que nous avons peut-être aujourd'hui perdu un important client, excédé de nos problèmes de performances. Ajoutons également que les DBAs nous facturent en interne une petite fortune, et l'on comprend pourquoi l'on songe à bouger.

De l'autre, Sybase, qui a l'énorme avantage de coûter quelque chose comme 20 fois moins au bas mot, grâce à la ristourne négociée par le département en échange d'un achat massif de licenses. Les performances pourraient être meilleures (?), mais les fonctionnalités semblent moins à la hauteur. L'absence de requêtes récursives est un problème qui pourrait être particulièrement difficile à émuler, et complexifier de fait la couche applicative. L'exécution de requêtes concurrentes pourrait être moins efficace. Et enfin, je n'ai toujours pas réussi à faire fonctionner leur bibliothèque ODBC en 64 bits.

Je n'aurais pas eu d'hésitations à proposer Postgresql, mais malheureusement ce n'est pas une option dans notre monde merveilleux. Tant pis, on fera avec.

samedi 10 mars 2012

Aller chercher la classe de base

Un test de C++, plutôt moyen au demeurant, m'a permis de découvrir une syntaxe pour appeler la fonction de base sur un type dérivé:


#include

class Base
{
public:
virtual ~Base() {}
virtual void f() { std::cout << "Base" << std::endl; }
};

class Derived : public Base
{
public:
virtual void f() { std::cout << "Derived" << std::endl; }
};

int main()
{
Derived d;
d.Base::f();
}

Au final, cette syntaxe est plutôt logique: depuis une méthode dérivée, on peut en effet appeler une méthode de base avec Base::f(), c'est à dire this->Base::f(). Donc, pourquoi pas sur un autre objet que this?

Badge tumbleweed

La plupart de mes questions de programmation ont déjà leur réponse sur StackOverflow, mais il arrive parfois que sur un sujet suffisamment précis, je me lance à poster.

Le résultat n'est pas tout à fait à la hauteur de mes attentes, mais j'en récupère cependant un avantage: je suis maintenant le détenteur du badge Tumbleweed, qui récompense les questions n'ayant reçu ni réponses, ni commentaires, ni votes pendant une semaine.

jeudi 1 mars 2012

Analysez vos tables

D'abord, créons une table raisonnablement grosse, avec un bel index créé implicitement par la clé primaire.


drop table if exists numbers;
create table numbers(n integer primary key);
insert into numbers select generate_series(1, 100000);

Si l'on veut chercher une valeur en particulier, l'on voit que le planificateur de tâches décide très raisonnablement de passer par l'index:

select * from numbers where n = 1297



Là où ça devient intéressant, c'est lorsque l'on passe par une table temporaire, parce que l'on peut vouloir chercher par exemple plusieurs valeurs à la fois:

create temporary table test(n integer not null);
insert into test values(1297);

Regardons le plan...

select * from numbers n join test t on n.n = t.n;

Stupeur, le planificateur décide de hacher l'ensemble de la table "numbers", au lieu d'utiliser l'index.


En effet: le planificateur, ne connaissant pas la taille de la table temporaire, suppose par défaut qu'elle fait 1000 lignes, et par conséquent choisit un plan non optimal.

Heureusement, il est possible de lui demander d'analyser la table afin de mettre à jour ses statistiques.

Ansi:

analyze test;
select * from numbers n join test t on n.n = t.n;

nous donne enfin le plan recherché.


L'on peut ainsi dépenser quelques millisecondes au sein de la transaction pour mettre à jour ses statistiques, et sauver ainsi potentiellement plusieurs secondes en permettant au planificateur de mieux choisir.

dimanche 26 février 2012

Dear Esther

Et un autre ovni vidéo-ludique qui débarque sur nos plateformes. Attiré par la pub sur Steam et par les commentaires énigmatiques sur de nombreux sites de jeux, je me suis offert la chose.

Il s'agit donc d'une histoire à la première personne où le joueur se ballade de manière complètement linéaire dans une île, divers endroits déclenchant un petit commentaire du narrateur, faisant progresser l'histoire.

En terme de gameplay, c'est très limité: il n'y a aucune interaction avec le décor, aucun obstacle, rien de caché, il suffit en gros de suivre le chemin. L'on peut explorer à droite à gauche, et découvrir de fabuleux paysages, mais l'on est rapidement stoppé par des rochers, une barrière, ou la mer. Au pas de course, le jeu se complète en 40 minutes. En prenant son temps, l'on peut aller jusqu'à 2 heures.

En terme d'atmosphère, par contre, c'est autre chose: l'on découvre petit à petit l'histoire, au milieu de paysages sauvages tout à fait remarquables. Le chapitre se passant dans une grotte est particulièrement beau, la musique est magnifique, et le sujet parfois poignant.

Certains trouverons le prix (environ 8€, ou 7£) trop cher pour une bonne heure de ballade, d'autres s'extasieront sur le concept novateur. J'ai essayé, et je suis satisfait.

vendredi 24 février 2012

C'était trivial...

Et Médoc utilise maintenant Tesseract 3.

Il faudra peut-être penser à paralléliser tout ça, parce que pour l'instant, le moins que l'on puisse dire est que l'analyse n'est pas particulièrement fulgurante. Mais les résultats sont là.

Tesseract 3.02 dans les bacs!

Yay, enfin, Tesseract 3 est disponible dans Wheezy!

Comme prévu, c'est le jour et la nuit en comparaison avec l'interface précédente. En plus des points exposés dans le précédent post sur le sujet, il est bon de rappeler également que Tesseract 3 renvoie un résultat en UTF8. J'ai bien l'intention de faire quelques tests avec le mode Mandarin et le mode équations, que je me ferai un plaisir de partager ici.

jeudi 16 février 2012

Danse et algorithmique

Un quick-sort par des danseurs hongrois. C'est bien plus clair comme cela, non?

mardi 14 février 2012

Tesseract 3.02 se rapproche de Wheezy

Voilà un paquet que j'attends avec impatience. Tesseract 3.0 a atterri dans Sid il y a 8 jours, et si tout va bien, devrait débarquer dans Wheezy très bientôt.

Au menu:
- Une API plus propre, et thread safe, ce qui permettra l'analyse des pages en parallèle (à 30 secondes par page, c'est plutôt utile!)
- Et surtout, une meilleure gestion des erreurs, qui permettra de renvoyer un joli message plutôt que de crasher et laisser le développeur découvrir où il s'est trompé. De plus, certains crashs "incompréhensibles" devraient être résolus.

J'ai hâte d'intégrer tout cela à Médoc.

dimanche 12 février 2012

Debug

Je viens de passer une semaine à débugger. Dans une codebase de 1.5 millions de lignes, un serveur qui corrompt les données, au bout d'un moment, lorsque deux clients y sont connectés et font suffisamment de requêtes.

Ce cas de figure est le pire qui se présente: mes tests unitaires passent, mes tests simples passent, mais mes tests pour stresser le système foirent en beauté, mais de manière suffisamment aléatoire pour que je ne puisse pas arrêter le débuggeur au moment où le problème surgit. Je ne peux qu'ajouter de plus en plus de lignes de logs au moment où le problème survient, en espérant enfin comprendre pourquoi.

De jour en jour, j'ai réussi à cerner le problème: en détectant la corruption de plus en plus près de sa cause, j'ai réussi à m'approcher suffisamment pour voir exactement le cheminement de l'exécution, et trouver le coupable.

Comme beaucoup de bugs de ce genre, c'était tout bête, planqué dans une vieille partie de code.

En voici le pseudocode:


template<typename T>
size_t hash(T input)
{
static int buffer[4];
calculate_hash((char *)&input, sizeof(input), buffer);
return *((size_t *)buffer);
}

L'on créé un buffer de travail, l'on appelle la fonction calculate_hash en lui passant l'entrée et le buffer de travail, et l'on retourne le début du buffer.

Dans le principe, cela marche très bien. Sauf que regardez d'un peu plus près le mot-clé "static". Le développeur était un peu endormi ce jour là, et l'a mis par erreur. Dans un programme non concurrent, comme par exemple un test unitaire, ça n'a pas beaucoup de conséquences. Peut-être rend-il le code un peu moins efficace, puisque sinon l'on travaillerait sur la pile, laquelle a de fortes chances d'être dans les caches du processeur. Par contre, dans un programme concurrent, c'est la catastrophe: si deux threads appellent la fonction au même moment, le buffer sera partagé, et les résultats imprédictibles. En l’occurrence, mes tables de hachage se retrouvaient à me renvoyer, une fois de temps en temps, une valeur incorrecte, quand le programme était très occupé.

Mot-clé retiré. Tout fonctionne.

dimanche 5 février 2012

Emacs - Changer la fonte

À chaque semaine et à chaque mise à jour Debian, son lot d'aventures. Aujourd'hui, c'est Emacs qui change sa fonte sans prévenir. C'est l'opportunité d'en reprendre une lisible et belle. Après quelques tentatives à coups de "Options" => "Set Default Font", mon choix se porte sur DejaVu Sans Mono, très lisible, monospace (impératif pour écrire du code), et avec un zéro bien différentié du o majuscule.

Un petit

(set-default-font "DejaVu Sans Mono 10")
dans mon .emacs plus tard, et tout est réparé.

Skyrim toujours pas en promo

J'ai raté les promos du début de l'année sur Steam, et j'en suis fort marri. Je suis dans les starting blocks dès que les prix diminuent.

dimanche 29 janvier 2012

L'oeil extérieur

Depuis que quelques bonnes âmes essayent Médoc, je retrouve nombre de petites limitations, ou de cas particuliers que j'avais ignoré un peu rapidement. Le résultat, c'est que le logiciel s'améliore lentement, mais sûrement.

Ce sont des choses bêtes: la localisation, par exemple, qui envoyait des dates en français à la base de données. Ou le support multi-devices: Sane ne reconnait que mon scanner HP, j'ai donc paresseusement simplifié Médoc en prenant le premier device disponible. Mais une simple webcam, par exemple, est considéré comme étant un device supplémentaire, et tout d'un coup, l'hypothèse ne tient plus.

La morale de cette histoire, c'est:
- Eat your own dog food
- Mais faites tester par d'autres!

dimanche 22 janvier 2012

Un client Twitter en OCaml

Pourquoi s'arrêter en si bon chemin? Voici un nouvel œuf de canard: le même client Twitter pour l'API stream, mais cette fois-ci en OCaml. Utilisant Yojson et OCurl, c'est le port copie conforme du client C++.

Si jamais je me décidais à enregistrer les messages dans une base de données, c'est plutôt à partir du client OCaml que je continuerais.

mardi 17 janvier 2012

Oeuf de canard - C++ et zlib

Un vieil œuf de canard que je viens de poster: mon vieux wrapper zlib est maintenant dans le nid douillet de Github, avec deux binaires triviaux pour la compression et la décompression.

vendredi 13 janvier 2012

Peaufinage

Quelques améliorations de Médoc: tout d'abord, je rends l'OCR optionnel, puisque la version 2 de Tesseract, empaquetée sous Debian, est assez buggée et crashe régulièrement. Si un document ne passe pas, allez donc désactiver l'option.

Ensuite, j'ai tenté d'améliorer le look de la boite de dialogue du scan: la bibliothèque Sane contient déjà la notion de groupe, il suffisait donc de recoller les morceaux. Une boite de dialogue typique ressemble à ça:

Notez également que la barre de progression est maintenant intégrée, ce qui l'évite de surgir sur le bureau alors que l'on est en train de faire autre chose pendant un long scan.

Un gros morceau sera de revoir en détail l'ordre des contrôles dans chaque boite de dialogue, afin de simplifier la navigation par la touche Tab.

dimanche 8 janvier 2012

Streaming API Twitter

Non, je ne me suis pas encore vendu aux médias sociaux, mais puisque je pourrais en avoir besoin bientôt, je me suis lancé dans l'écriture d'un tout petit programme en C++ destiné à lire les feeds Twitter en temps réel. Combinant curl et json_spirit, suffisamment grand pour ne pas être pratique à poster, mais suffisamment petit pour ne pas valoir un projet à lui tout seul, c'était le candidat parfait pour mon dépôt "Œufs de canard" dont je parlais dans un post précédent (j'avais en effet déjà une idée derrière le bec).

Le code se trouve ici. L'on lancera le programme ainsi:

./twitterstream -u username -p password -k mot1 -k mot2 -k mot3...

Et devraient surgir à l'écran les messages filtrant sur un des mots-clé passés dans les options -k.

Je suis impressionné par le débit: sur des mots relativement courants, ça descend à toute vitesse.

Je me tâte maintenant à ajouter une couche de persistance vers une base de données... Peut-être après Médoc v2!

Oeufs de canard

J'inaugure mon nouveau dépôt Œufs de canard, où je placerai mes programmes de test, ce qui sera plus simple que de faire d'interminables listings plus ou moins bien formatés ici. Plus de contenu, moins de blabla, voici mes vœux pour ce blog en 2012 (vous allez me dire, ça commence bien...).

samedi 7 janvier 2012

J'oubliais...

Et bonne année, aussi!

vendredi 6 janvier 2012

Covariance

C'est étrangement une fonctionnalité qui semble relativement peu connue des programmeurs C++: la covariance des types de retours dans les fonctions surchargées.

L'exemple standard est la déclaration d'une méthode "clone", qui renvoie une copie de l'objet. Prenons une classe A:


class A
{
public:
virtual ~A();
virtual A * clone() const;
};

Maintenant, comment écrire une classe B dérivant de A, de manière à ce que la méthode clone fonctionne, c'est à dire renvoie une instance de B si le type sous-jacent est B? Ou plus exactement, quel devrait être le type de retour de la méthode clone de B?

L'on peut bien entendu retourner A. C'est clair, net et précis lorsque l'on travaille avec A, mais c'est dommage lorsque l'on travaille directement avec B: appeler clone renvoie un pointeur sur A, alors que l'on sait pertinemment que c'est un B, et on doit donc le caster vers B pour travailler avec.

Profitons donc de cette fonctionnalité introduite dans C++98, qui dit que si une méthode virtuelle renvoie un pointeur, l'on peut surcharger cette méthode avec un type de retour pointeur vers un objet dérivé. L'on écrira donc:

class B : public A
{
public:
B * clone() const;
};

Et la méthode clone fonctionnera exactement de la manière désirée.

Il est à noter que le type de retour peut-être différent du type parent: si A dérive de B, et C dérive de D, une méthode de A retournant C peut être surchargée dans B comme retournant D.

Il est à noter également que malheureusement, cette fonctionnalité devient de moins en moins utile: de nos jours, il est de plus en plus rare de renvoyer un pointeur à poil, et il n'est pas possible d'obtenir le même résultat avec un shared_ptr, par exemple. Dans ce cas, il faudra renvoyer systématiquement le type parent, et faire un cast.