J'ai regardé le code assembleur et ma méthode Database::get est bien présente dans le fichier objet, et appelée le bon nombre de fois (callgrind confirme). Le benchmark ne portant que sur la lecture de la base de donnée, la seule chose qui peut arriver est que la base de donnée tienne dans le cache du processeur. Des parties peuvent être contenues dans ce cache (qui fait 3 Mio), mais pas tout, la base de donnée fait 12 Mio.
Entre la version non thread-safe et la version thread-safe, le seul code qui change au niveau de la lecture de la base de donnée (testée dans le benchmark) est l'ajout de ceci :
voidPerThreadAccess::enterDatabase(){while(true){_db_accessed=true;store_fence();if(_db_access_allowed){break;}else{// Waiting for the db to be accessible is a very long operation// (the database file has to be remapped, pagefaults may occur, etc)// Don't spin, but yield the processor to an useful thread_db_accessed=false;store_fence();while(!_db_access_allowed)sched_yield();}}}voidPerThreadAccess::leaveDatabase(){_db_accessed=false;}
Ces deux méthodes sont appelées juste avant et juste après la lecture. store_fence() est une méthode inline qui exécute l'instruction assembleur sfence, qui garanti qu'aucune lecture après cette instruction (donc de la base de donnée, et surtout de _db_access_allowed) n'aura lieu avant que l'écriture de _db_accessed soit visible par tous les autres coeurs de la machine. Le code assembleur contient bien toutes les instructions nécessaires, donc l'inlining, bien qu'agressif (c'est pour ça qu'on utilise un compilateur, surtout que j'ai tout compilé en LTO), n'a pas modifié le comportement du programme.
Ce bout de code vient du fait que dans de très rares cas, tous les accès à la base de donnée doivent être suspendus (aussi bien en lecture qu'en écriture). En effet, quand la base de donnée devient trop petite pour ce qu'on veut y stocker, il faut agrandir le fichier, et le re-mapper. Re-mapper le fichier va très probablement invalider tous les pointeurs vers ce fichier mappé, et donc aucun accès ne peut être en cours.
L'algorithme est donc :
Placer les _db_access_allowed de tous les threads à false, les threads évitent dès lors de commencer un accès
Boucler et faire un sched_yield tant que tous les threads n'ont pas placé leur _db_accessed à false, donc tant qu'ils accèdent à la base de donnée
Agrandir et re-mapper le fichier
Placer tous les _db_access_allowed à true, les threads peuvent reprendre leur activité
L'utilisation de sched_yield n'est peut-être pas la plus élégante, et je n'ai pas encore suffisamment testé la base de donnée en condition réelle pour voir si ça accélère ou ralentit les choses par rapport à une busyloop. Sous Linux, il y a aussi les futex qui pourraient être utilisés pour endormir les threads tant que leur flag _db_access_allowed n'est pas à true, je regarderai de ce côté-là quand j'en aurai l'occasion (même si idéalement, déjà que pas mal de code est spécifique à x86, j'aimerais éviter de trop tomber dans le Linux-only).
Pour info, l'écriture en base de donnée (première phase du benchmark) est plus normale : il fallait avant 16 ms pour écrire toutes les clés, il en faut maintenant 46. L'écriture a donc été fortement impactée, mais ce n'est pas grave car cette base de donnée passe son temps à être lue (ce serait grave d'avoir un programme qui génère 250 Mio de données par seconde, en tous cas pour ce que je fais).
[^] # Re: Ai-je bien compris ?
Posté par steckdenis . En réponse au journal Performances des processeurs Intel et optimisation. Évalué à -1.
J'ai regardé le code assembleur et ma méthode
Database::getest bien présente dans le fichier objet, et appelée le bon nombre de fois (callgrind confirme). Le benchmark ne portant que sur la lecture de la base de donnée, la seule chose qui peut arriver est que la base de donnée tienne dans le cache du processeur. Des parties peuvent être contenues dans ce cache (qui fait 3 Mio), mais pas tout, la base de donnée fait 12 Mio.Entre la version non thread-safe et la version thread-safe, le seul code qui change au niveau de la lecture de la base de donnée (testée dans le benchmark) est l'ajout de ceci :
Ces deux méthodes sont appelées juste avant et juste après la lecture.
store_fence()est une méthode inline qui exécute l'instruction assembleursfence, qui garanti qu'aucune lecture après cette instruction (donc de la base de donnée, et surtout de _db_access_allowed) n'aura lieu avant que l'écriture de _db_accessed soit visible par tous les autres coeurs de la machine. Le code assembleur contient bien toutes les instructions nécessaires, donc l'inlining, bien qu'agressif (c'est pour ça qu'on utilise un compilateur, surtout que j'ai tout compilé en LTO), n'a pas modifié le comportement du programme.Ce bout de code vient du fait que dans de très rares cas, tous les accès à la base de donnée doivent être suspendus (aussi bien en lecture qu'en écriture). En effet, quand la base de donnée devient trop petite pour ce qu'on veut y stocker, il faut agrandir le fichier, et le re-mapper. Re-mapper le fichier va très probablement invalider tous les pointeurs vers ce fichier mappé, et donc aucun accès ne peut être en cours.
L'algorithme est donc :
L'utilisation de sched_yield n'est peut-être pas la plus élégante, et je n'ai pas encore suffisamment testé la base de donnée en condition réelle pour voir si ça accélère ou ralentit les choses par rapport à une busyloop. Sous Linux, il y a aussi les futex qui pourraient être utilisés pour endormir les threads tant que leur flag _db_access_allowed n'est pas à true, je regarderai de ce côté-là quand j'en aurai l'occasion (même si idéalement, déjà que pas mal de code est spécifique à x86, j'aimerais éviter de trop tomber dans le Linux-only).
Pour info, l'écriture en base de donnée (première phase du benchmark) est plus normale : il fallait avant 16 ms pour écrire toutes les clés, il en faut maintenant 46. L'écriture a donc été fortement impactée, mais ce n'est pas grave car cette base de donnée passe son temps à être lue (ce serait grave d'avoir un programme qui génère 250 Mio de données par seconde, en tous cas pour ce que je fais).