Support de formation sur les évolutions en C++11 concernant:
- L'initialisation dynamique des variables statiques non-locales (le "static initialisation order fiasco")
- La thread-safety du singleton via les "magic statics"
- Le thread-safety du Double-Checked Locking Pattern via les atomiques
2. Disclaimer
• Vous n'allez rien apprendre d'utile ☺
– En C++03, les variables globales c'est le mal...
– En C++11, c'est un peu mieux...
• Au menu
– Le static initialization order fiasco (SIOF)
– Le problème du singleton en C++03
– Le double-checked locking pattern (DCLP)
– Les magic statics gcc et C++11
4. Les variables statiques non-locales
• Ce sont
– Les variables globales
– Les variables membres statiques
• Initialisation
– Statique: à la compilation
– Dynamique: au lancement du programme
6. • Si on est chanceux, ça marche...
• Mais si on n'a pas de chance...
Compter jusqu'à 2... ou pas!
7. Qu'est-ce qui se passe?
Initialisation
Ordre des
initialisations
Un.c puis Deux.c Deux.c puis Un.c
1ère initialisation UN = abs(1) = 1 DEUX = UN +1 = 0 + 1 = 1
2ème initialisation DEUX = UN + 1 = 1+ 1 = 2 UN = abs(1) = 1
➢ L'ordre d'initialisation des variables est indéterminé
8. Le compilateur est buggué?
Objects with static storage duration defined in
namespace scope in the same translation unit
and dynamically initialized shall be initialized
in the order in which their definition appears
in the translation unit.
➢ Pour des variables statiques non-locales définies dans un même fichier
source, l'initialisation dynamique se fait dans l'ordre
➢ Aucune contrainte si les variables sont définies dans des fichiers séparés
C++03, clause 3.6.2, "Initialization of non-local objects"
9. C'est le static initialization order fiasco (SIOF)
What’s the “static initialization order fiasco”?
A subtle way to crash your program.
Suppose you have two static objects x and y in
separate source files, say x.cpp and y.cpp.
Suppose that the initialization for y calls some
method on x.
That’s it. It’s that simple.
You have a 50%-50% chance of dying.
Marshall Cline, Bjarne Stroustrup et al., "C++ Super-FAQ"
10. Qu'a changé C++11?
Static-duration object initialization may constitute a large
fraction of process time, particularly when users are waiting
for program start, and hence may benefit from parallel
execution.
In the sequential 2003 standard, unspecified order of
initialization essentially required some serial order of
initialization.
In the parallel proposed standard, the unspecified order of
initialization admits concurrent initialization.
L. Crowl, n2660, "Dynamic Initialization and Destruction with Concurrency"
➢ C++11 parallélise l'initialisation pour accéler le lancement des
programmes
11. En C++11, le SIOF se parallélise
➢ Dans un même fichier source: inchangé (sauf cas particuliers)
➢ Fichiers distincts en monothread: inchangée (ordre indéterminé)
➢ Fichiers distincts en multithread: initialisation concurrente possible
Variables with ordered initialization defined within a single
translation unit shall be initialized in the order of their
definitions in the translation unit.
If a program starts a thread, the subsequent initialization of a
variable is unsequenced with respect to the initialization of a
variable defined in a different translation unit.
Otherwise, the initialization of a variable is indeterminately
sequenced with respect to the initialization of a variable defined
in a different translation unit.
C++11 clause 3.6.2, "Initialization of non-local variables"
12. La solution c'est le singleton!?
Move each non-local static object into its own
function, where it's declared static.
These functions return references to the objects they
contain.
In other words, non-local static objects are replaced
with local static objects.
Aficionados of design patterns will recognize this as a
common implementation of the Singleton pattern.
Scott Meyers, "Effective C++, 3rd ed.", 2005
➢ Le singleton est la solution la plus courante au SIOF
➢ Il repose sur l'initialisation dynamique des variables statiques locales
14. Les variables statiques locales
• Ce sont
– Les statiques définies dans une fonction / un bloc
• Initialisation
– Au 1er passage dans la fonction / le bloc
15. Deux variantes possibles du singleton
Singleton retournant
une référence
Singleton retournant
un pointeur
➢ Il y a de nombreuses autres variantes
➢ On trouve également des templates de singletons
17. Le singleton se démultiplie...
Sans lock, l'initialisation du singleton n'est pas thread-safe...
... on peut le construire plusieurs fois ou en fabriquer plusieurs
21. Le singleton à pointeur s'atomise...
Sans lock, l'initialisation du singleton n'est pas thread-safe...
... on peut obtenir un pointeur nul
... on pourrait obtenir un pointeur vers un objet en construction
... on pourrait obtenir plusieurs pointeurs différents
22. Le singleton c'est pas la solution?
Avoid using a lazily-initialized Singleton unless
you really need it.
Use eager initialization instead, i.e., initialize a
resource at the beginning of the program run.
Initializing a singleton resource during single-
threaded program startup is the simplest way to
offer fast, thread-safe singleton access.
S. Meyers et A. Alexandrescu, "C++ and the Perils of Double-Checked Locking",
2004
➢ Meyers recommandait le singleton comme solution au SIOF
➢ Mais en multithread, le singleton pose de nouveaux problèmes
23. 3 états, 2 valeurs, 1 problème
Etat de la variable statique Etat observable
Avant initialisation Pas encore initialisé
Pendant l'initialisation Veuillez patienter???
Après initialisation Initialisé
➢ Il faut un troisième état, "Initialisation en cours, veuiller patienter"...
➢ Bref, il faut un mutex
25. Exemples de singleton mutexé
Singleton retournant
une référence
Singleton retournant
un pointeur
26. Singleton mutexé et performances
Visual Studio 2013 / x86 gcc 4.9.3 / cygwin x64
sans magic statics
Sans mutex 100 ms 60 ms
Avec mutex 6.3 s 11.5 s
Durée d'exécution pour 100 millions d'appels à getInstance()
➢ Le mutexage du singleton, ça n'est pas vraiment gratuit ☹
27. Singleton mutexé et efficacité
Gestion du mutex pour 10 millions d'appels à getInstance()
➢ Le mutex n'est utile que pendant la phase d'initialisation
➢ Après, il ne sert à rien, mais on doit le verrouiller quand même ☹
Itération Comportement
1 Verrouillage du mutex pour construction thread-safe
2 Verrouillage du mutex parce qu'il est là n°1
3 Verrouillage du mutex parce qu'il est là n°2
...
10 000 000 Verrouillage du mutex parce qu'il est là n°9 999 999
28. Le Double-Checked Locking Pattern (DCLP)
Every call to instance must acquire and release the
lock.
Although this implementation is now thread-safe,
the overhead from the excessive locking may be
unacceptable.
A better way to solve this problem is to use Double-
Checked Locking, which is a pattern for optimizing away
unnecessary locking.
Douglas C. Schmidt, "Double-Checked Locking, An Optimization Pattern for
Efficiently Initializing and Accessing Thread-safe Objects", 1997
➢ Le DCLP vise à ne prendre le mutex que quand c'est nécessaire
29. Principe du Double-Checked Locking Pattern
• On vérifie si le singleton est déjà initialisé
1. S'il ne l'est pas, on verrouille le mutex
2. On vérifie que le singleton n'a pas été initialisé pendant qu'on attendait le lock
3. Si personne n'a initialisé le singleton dans l'intervalle, on l'initialise
30. Mais DCLP est cassé (Java 1.4) ☹
• Double-Checked Locking is widely cited and used as an
efficient method for implementing lazy initialization in a
multithreaded environment.
• It doesn't work
• There are lots of reasons it doesn't work.
• After understanding those, you may be tempted to try to
devise a way to "fix" the double-checked locking idiom.
• Your fixes will not work: there are more subtle reasons why
your fix won't work.
• Understand those reasons, come up with a better fix, and it
still won't work, because there are even more subtle reasons.
David Bacon et coll., The "Double-Checked Locking is Broken" Declaration,
2000
31. En C++03 aussi, DCLP est cassé ☹
DCLP is designed to add efficient thread-safety to initialization of
a shared resource (such as a Singleton), but it has a problem:
it’s not reliable.
Furthermore, there’s virtually no portable way to make it
reliable in C++ (or in C) without substantively modifying the
conventional pattern implementation.
To make matters even more interesting, DCLP can fail for
different reasons on uniprocessor and multiprocessor
architectures.
S. Meyers et A. Alexandrescu, "C++ and the Perils of Double-Checked Locking",
2004
32. Qu'est-ce qui ne marche pas?
1
2
3
4
5
➢ DCLP repose sur un ordre d'exécution très précis
➢ Mais qu'est-ce qui se passe si cet ordre n'est pas respecté?...
33. Compilation et ordre d'exécution
➢ Conforming implementations are required to
emulate (only) the observable behavior of the [C++]
abstract machine.
➢ This provision is sometimes called the “as-if” rule,
because an implementation is free to disregard any
requirement of this International Standard as long as
the result is as if the requirement had been obeyed,
as far as can be determined from the observable
behavior of the program.
➢ Un compilateur peut réordonner tant que le comportement est inchangé
➢ En C++03, le modèle d'exécution est mono-thread
➢ Le compilateur peut réordonner certaines opérations, et casser DCLP
C++03, clause 1.9, "Program execution"
34. Architecture et ordre d'exécution
➢ Les opérations peuvent être réordonnées selon l'architecture
➢ L'architecture peut réordonner certaines opérations et casser DCLP
P. E. McKenney, "Memory Barriers: a Hardware View for Software Hackers", 2009
35. Réparer DCLP avec des barrières... ☹
• To use the DCLP Optimization correctly on some platforms,
CPU-specific instructions must be inserted.
D. C. Schmidt, "Pattern-Oriented Software Architecture" Vol. 2, 2000
37. Ce que change C++11
• Concurrency introduces potential deadlock or data races in
the dynamic initialization and destruction of static-duration
objects.
• The language must introduce new syntax, define
synchronization, or limit programs.
• This proposal breaks the problem into three reasonably
separable parts:
– initialization of function-local static-duration objects,
– initialization of non-local static-duration objects,
– and destruction of all static-duration objects.
L. Crowl, n2148, "Dynamic Initialization and Destruction with Concurrency"
38. Les magic statics
➢ L'initialisation des variables statiques locales devient thread-safe
➢ Implémenté sous gcc depuis gcc 4.3 (a servi de base au standard)
➢ Implémenté sous Visual Studio dès VS 2015
[A variable with static or thread storage duration] is
initialized the first time control passes through its
declaration.
If control enters the declaration concurrently while
the variable is being initialized, the concurrent
execution shall wait for completion of the initialization.
C++11 clause 6.7, "Declaration statement"
39. 3 états, 2 valeurs, 1 solution
Etat de la variable statique Etat observable
Avant initialisation Pas encore initialisé
Pendant l'initialisation Veuillez patienter...
Après initialisation Initialisé
➢ On a notre troisième état, "Initialisation en cours, veuiller patienter"
40. Le singleton devient thread-safe
Singleton retournant
une référence
Singleton retournant
un pointeur
➢ La thread-safety de l'initialisation est intégrée dans le langage
➢ Utiliser un mutex ou DCLP devient inutile
41. Sous le capot: les magic statics de gcc
➢ C'est DCLP en assembleur
➢ Le compilateur sait implémenter au mieux selon l'architecture
if (initialized) return instance;
acquire(initialisation guard);
if (initialized) return instance;
instance = new Singleton();
release(initialisation guard);
return instance;
42. En C++03, on pouvait réparer DCLP avec Boost ou TBB
➢ Les libraries fournissaient des objets atomiques
➢ Les libraries géraient l'ordre d'exécution (opaque calls, assembleur...)
43. En C++11, les objets atomiques intègrent le langage
➢ La syntaxe C++11 est un peu plus lourde que pour Boost ou TBB
➢ L'ordre d'exécution est garanti par le langage (memory model)
44. Les atomiques C++11 permettent des optimisations fines
➢ Ce type de fine-tuning est à utiliser avec précaution
Jeff Preshing, "Double-Checked Locking is Fixed In C++11"
45. Singleton, thread-safety et performances
Durée d'exécution pour 100 millions d'appels à getInstance()
➢ Les statiques locales C++11 sont thread-safe et efficaces
➢ Implémenter DCLP pour un singleton est inutile
Thread-safe?
Visual Studio 2013
x86
gcc 4.9.3
cygwin x64
statique C++03 NON 100 ms 60 ms
statique C++03 mutexée OUI 6.3 s 11.5 s
DCLP thread-unsafe NON 410 ms 50 ms
DCLP avec atomic TBB OUI 400 ms 240 ms
DCLP avec atomic C++11 OUI 400 ms 70 ms
Magic static C++11 OUI N/A 70 ms
46. Qu'est-ce que ça change
pour le développement multi-cible?
Environnement
Windows
Environnement Linux
Statiques locales Thread-unsafe avec VS 2013 Thread-safe avec gcc
Magic statics Thread-safe avec VS 2015 et gcc
➢ Etre iso sur toutes les cibles, c'est mieux ☺
47. DCLP sert encore à quelque chose?
➢ Le singleton est un cas particulier de l'idiome d'initialisation tardive (lazy)
➢ Les objets doivent être connus à l'avance (un objet ⇔ une magic static)
➢ DCLP reste utile si le nombre d'objets est variable (cache dynamique)
Calcul et caching de la
somme des données
L'accumulateur est construit
à partir d'un vecteur de
données
DCLP pour implémenter un
calcul lazy de la somme
des données
48. DCLP dans le langage avec std::call_once
➢std::call_once encapsule l'appel à une fonction
➢ La fonction ne sera appelée qu'une seule fois au maximum
50. Qu'est-ce qui change en C++11?
• Initialisation dynamique des statiques non-locales
– Pas de changement majeur en monothread (SIOF!)
– En multithread, l'initialisation peut être concurrente
• Initialisation des statiques locales et singleton
– C++11 intègre les magic statics
– L'initialisation du singleton est toujours thread-safe
– DCLP est inutile pour le singleton
• DCLP
– DCLP est réparé avec les atomiques et le memory-model
– A connaître pour le caching dynamique
– Peut-être réalisé avec std::call_once
51. Disclaimer
• Vous n'avez rien appris de très utile... ☺
• Mais quand même...
– Pensez au static initialization order fiasco
– Pensez à DCLP/call_once pour les caches dynamiques
– Remplacer les atomiques Boost/TBB par C++11?
• Au menu pour un prochain tutorial
– Grandeur et déclin du mot-clé volatile
– Le memory model de C++11