dimanche 24 avril 2011

Performances: Asio custom alloc

Reprenant le post précédent, je me suis demandé quel était l'effet des allocations sur les performances. Puisqu'Asio permet de customiser ses allocateurs, j'ai adapté l'exemple fourni par Asio à ma comparaison boost::bind contre lambda.

Tout d'abord, Valgrind confirme effectivement que les allocations sont parties (test réduit à 2000 tâches, sinon ça prenait la nuit):


==3194== Memcheck, a memory error detector
==3194== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==3194== HEAP SUMMARY:
==3194== in use at exit: 0 bytes in 0 blocks
==3194== total heap usage: 2,006 allocs, 2,006 frees, 96,496 bytes allocated

==3205== Memcheck, a memory error detector
==3205== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==3205== HEAP SUMMARY:
==3205== in use at exit: 0 bytes in 0 blocks
==3205== total heap usage: 6 allocs, 6 frees, 496 bytes allocated

Ensuite, étonnamment, les performances ne sont pas si différentes:



J'avais imaginé que le million d'allocations prendrait la part du lion dans l'exécution du programme, mais de fait l'on ne parle que d'une différence de 10%. C'est toujours bon à prendre, mais loin d'être un miracle.

Pour un gain de 0.15μs par allocation, il me semble inutile de se fatiguer à passer par un allocateur fait à la mimine, à part peut-être dans les cas extrêmes où l'on cherche à réduire la latence par tous les moyens.

mercredi 20 avril 2011

Performances: lambda vs boost::bind

Maintenant que l'on a les lambdas, peut-on se passer de boost::bind? Et qui est le plus rapide?

Voici un petit programme qui fait tourner un certain nombre de "tâches" de manière asynchrone à travers le framework asio, en passant un handler soit via un (une?) lambda, soit un boost::bind.


#include <iostream>
#include <chrono>

#include <boost/asio.hpp>
#include <boost/bind.hpp>


class Recursive
{
public:
Recursive(boost::asio::io_service & io_service,
bool lambda,
int max):
m_io_service(io_service),
m_lambda(lambda),
m_max(max)
{
}

void run(int i)
{
if(i < m_max)
{
if(m_lambda)
{
m_io_service.post([this, i](){run(i + 1);});
}
else
{
m_io_service.post(boost::bind(&Recursive::run,
this,
i + 1));
}
}
}

private:
boost::asio::io_service & m_io_service;
bool m_lambda;
int m_max;
};

int main()
{
{
boost::asio::io_service io_service;
Recursive rec(io_service, true, 10000000);
io_service.post([&rec](){rec.run(0);});

auto t1 = std::chrono::system_clock::now();

io_service.run();

auto t2 = std::chrono::system_clock::now();
std::cout << "lambda: " << (t2 - t1).count() << std::endl;
}

{
boost::asio::io_service io_service;
Recursive rec(io_service, false, 10000000);
io_service.post([&rec](){rec.run(0);});

auto t1 = std::chrono::system_clock::now();

io_service.run();

auto t2 = std::chrono::system_clock::now();
std::cout << "bind: " << (t2 - t1).count() << std::endl;
}
}


Eh bien, les braves petites lambdas sont significativement plus rapides que boost::bind, d'environ 5 à 6 %. Il ne s'agit pas d'allocations, valgrind confirmant que les allocations mémoire restent les mêmes. Peut-être le foncteur correspondant à un boost::bind est-il plus compliqué à construire.

mercredi 13 avril 2011

Fglrx sur Debian - Encore des histoires

Ça faisait longtemps que ça n'avait pas foiré, j'avais donc perdu l'habitude. Le retour de manivelle fut rude: avec l'arrivée du noyau 2.6.38, le serveur X m'a sauté à la figure avec ses habituels "fglrx(0): ACPI: DRM connection failed", "atiddxDriScreenInit failed", pour finir par segfaulter lamentablement.

Une petite recherche finit par me convaincre que le module noyau n'était tout simplement pas présent. À l'installation et à la désinstallation du module fglrx-modules-dkms, je voyais bien la compile pour les noyaux 2.6.30 et 2.6.32, mais pas pour le nouveau menu.

Finalement, c'était tout bête. Il me fallait simplement les kernel headers 2.6.38. Un petit "aptitude install linux-headers-2.6.38-2-amd64", lequel automatiquement construit tous les modules nécessaires, et je suis de retour avec l'accélération matérielle.

Ouf.

mercredi 6 avril 2011

Postgresql et Ocaml: performances à l'insertion

Postgresql permet 2 types d'insertions: les insertions en pur SQL à l'aide d'un "insert", et les copies. À quel point les copies sont-elles plus efficaces que les insertions?

Voici quelques tests rapides via le mode immédiat d'Ocaml, en passant par le wrappeur Postgres. Ouvrons tout d'abord une connexion.


ocaml -I /usr/lib/ocaml/postgresql/ -I /usr/lib/ocaml/threads
# #load "unix.cma";;
# #load "threads.cma";;
# #load "bigarray.cma";;
# #load "postgresql.cma";;
# let connectionstring = "host=localhost dbname=Test";;
# let c = new Postgresql.connection ~conninfo:connectionstring ();;

Voici quelques fonctions de base qui servent respectivement à créer une séquence de a à b, et à mesurer le temps pour exécuter une fonction donnée. Notez que "seq" est un peu alambiquée pour permettre la tail-recursion (et donc de demander des sequences de 1M d'éléments sans dépassement de pile).

# let seq a b =
let rec do_seq a r =
if a > b then r else do_seq (a + 1) (a::r)
in List.rev(do_seq a []);;
# let time f e =
let t1 = Unix.gettimeofday() in f e;
let t2 = Unix.gettimeofday() in
t2 -. t1;;

Et enfin, les deux fonctions à tester, l'une faisant tourner n insertions dans une transaction, l'autre utilisant la copie.

# let run n =
ignore(c#exec "begin");
List.iter
(fun e -> ignore(c#exec
~params:[|string_of_int e; "def"; "0.143"|]
"insert into data values($1, $2, $3)"))
(seq 1 n);
ignore(c#exec "commit");;
# let batch n =
ignore(c#exec "copy data from stdin");
List.iter
(fun e -> ignore(c#putline ((string_of_int e)^"\tdef\t0.143\n")))
(seq 1 n);
ignore c#endcopy;;

Ces fonctions font strictement la même chose, c'est à dire insérer pour i de 1 à n, {i, "def", 0.143} dans une table ayant pour colonnes un entier, une chaîne, et un double.

L'on fait tourner...


# time run 100000;;
- : float = 13.3781638145446777
# time batch 100000;;
- : float = 0.458184957504272461


Ce qui nous donne un déjà très honorable 7500 insertions par seconde avec des insert, et un ahurissant 220000 insertions par seconde avec une copie. Les gains sont encore plus significatifs avec plus de lignes, la copie atteignant 240000 insertions par seconde pour 1 million d'éléments.

Notez tout de même que ma base est configurée avec fsync = off afin d'éviter les coûteuses synchronisations avec le disque. Quand bien même il est vrai qu'attendre que tout soit écrit aplanirait les différences, un cas d'utilisation standard d'insertions massives est la jointure avec une table temporaire pour faire un select, par exemple, pour lequel les écritures disques ne rentreront pas en compte.

Ce qui nous fait donc quand même une insertion par copie 30 fois plus rapide qu'une insertion en SQL pur.

mardi 5 avril 2011

stream iterators

C'est un équilibre, il me faut ma dose de code. Et vu que j'en ai en quantités industrielles au boulot, j'ai plutôt tendance à me rattraper sur mes lectures une fois réintégré mon trou. Cependant, ce n'est pas une raison pour se laisser totalement abattre, et je me suis dit que j'allais mentionner une fonctionnalité que je me suis mis à utiliser récemment: les stream iterators. À ma grande honte, je dois admettre que j'en ai compris l'utilité en notant un exercice d'un candidat lors d'une de nos sessions de recrutement. L'ensemble était moyen, mais sa manière de lire les fichiers de données était tout à fait remarquable.

Exercice classique: étant donné un fichier contenant une liste d'entiers, les mettre dans un vecteur, et les afficher à l'écran.

Les solutions habituelles vont du plus manuel à coup de buffers et de recherche d'espaces, ou de caractères non numériques, à des choses plus propres de type boucles avec des ifstream. Sans parler des soucis à la fin du fichier, pour savoir quand s'arrêter, comment supporter un retour chariot final ou pas...

Avec les istream_iterator, c'est un plaisir: comme leur nom l'indique, on leur donne un istream, et on itère.


#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <iterator>

int main()
{
std::vector<int> myNumbers;

std::ifstream f("myfile.txt");
std::copy(std::istream_iterator<int>(f),
std::istream_iterator<int>(),
std::back_inserter(myNumbers));

std::for_each(myNumbers.begin(),
myNumbers.end(),
[](int i){std::cout << i << "\n";});
std::cout.flush();

return 0;
}

Et le résultat:

$ cat myfile.txt
1 123 33 2222 1425
78
11

$ ./test
1
123
33
2222
1425
78
11


J'avoue, je n'ai pas pu m'empêcher de coller une lambda. On ne se refait pas.

Alors certes, ce n'est que de l'encapsulation de fonctionnalités de base. Mais c'est pour moi le bon idiome pour ce genre d'activités, et j'aimerais voir plus de code se baser dessus, et moins sur des solutions à la mimine et trop souvent buggées.

La beauté de la chose apparaît complétement lorsque l'on se rend compte qu'on peut passer n'importe quel objet streamable en paramètre template. Créez votre objet, son opérateur de stream, et basta, lisez-en des listes et des listes.

Dernière remarque, un poil hors sujet: je crois que la beauté des stream et de la hiérarchie de classe associée n'apparaît vraiment que le jour où vous devez faire un test unitaire d'une horreur prenant un FILE *. Le triptyque istream pour la classe générique, istringstream pour les tests unitaires et ifstream pour la production resplendit au firmament de l'ingénierie logicielle.