Depuis Visual Studio 2002 (y compris le compilateur en ligne de commande, gratuit, du Microsoft Visual C++ Toolkit 2003 et les compilateurs 64 bits, également gratuits, du Windows 2003 server SP1 Platform SDK), le compilateur du Visual C++ comporte une option d'optimisation pouvant produire des résultats très intéressants : l'option « Whole Program Optimisation ».
En activant cette option, le compilateur n'utilise plus la séparation des tâches en deux temps bien distincts : la compilation séparée de chaque source C++ (ou C) en un fichier .obj par cl.exe, puis le lien entre toutes les fonctions et autres symboles par l'édition des liens par link.exe.
Au contraire, cl.exe ne génère d'abord qu'un code intermédiaire, et la génération du code binaire proprement dit n'a lieu que lors de l'édition des liens, en une seule fois. C'est un peu comme si on concaténait tous les fichiers .cpp (en remettant juste à zéro l'espace des symboles entre chaque) et que l'on compilait le résultat final.
Résultat : un code à la fois plus court et plus rapide, sans aucun autre inconvénient qu'une phase de compilation peut être un peu plus longue (et consommatrice d'un peu plus de mémoire).
Un seul défaut : si vous distribuez des .lib comprenant vos fonctions et compilé avec l'option /GL, les .lib seront spécifique à la version de Visual C++. Aucun problème évidemment si vous distribuez des EXE ou des DLL (y compris bien sûr avec leur .LIB d'import de fonction, qui ne contient pas de code).
Pour montrer tout cela, un petit exemple : une application C++ simpliste composée de deux fichiers sources.
/* democgl.h */
typedef
struct
{
long
l1;
long
l2;
}
DEMOSTRUCT;
void
DemoGL_DispL(DEMOSTRUCT*
pds);
void
DemoGL_IncL2(DEMOSTRUCT*
pds);
/* demofnc.cpp */
#include
<stdlib.h>
#include
<stdio.h>
#include
"democgl.h"
void
DemoGL_DispL(DEMOSTRUCT*
pds)
{
long
l3,i;
l3=
0
;
for
(i=
0
;i<
pds->
l1;i++
)
l3 -=
pds->
l1 +
pds->
l2;
for
(i=
0
;i<
pds->
l1;i++
)
l3 *=
pds->
l1 +
pds->
l2;
for
(i=
0
;i<
pds->
l1;i++
)
l3 +=
pds->
l1 +
pds->
l2;
for
(i=
0
;i<
pds->
l1;i++
)
l3 /=
pds->
l1;
printf("l1 is %u, l2 is %u
\n
"
,pds->
l1,pds->
l2,l3);
}
void
DemoGL_IncL2(DEMOSTRUCT*
pds)
{
pds->
l2+=
7
;
}
/* democgl.cpp */
#include
<stdlib.h>
#include
<stdio.h>
#include
<windows.h>
#include
"democgl.h"
int
main()
{
DEMOSTRUCT ds;
ds.l1 =
GetTickCount();
ds.l2 =
GetVersion();
DemoGL_DispL(&
ds);
ds.l1+=
2
;
DemoGL_DispL(&
ds);
ds.l1+=
3
;
DemoGL_IncL2(&
ds);
DemoGL_IncL2(&
ds);
DemoGL_DispL(&
ds);
}
Évidemment, ce programme ne fait que des calculs inutiles. Mais il permet de démontrer le travail de l'optimisation. Analysons-le.
Notre fonction DemoGL_IncL2 modifie le membre l2 de la structure DEMOSTRUCT, mais laisse l1 constant. Par contre DemoGL_DispL ne modifie en rien la structure passée en paramètre. Un bon programmeur aurait du déclarer le type const DEMOSTRUCT* pour le paramètre, mais il était superflu de mettre la puce à l'oreille du compilateur qui (on le verra) se débrouille très bien tout seul !
Ensuite, dans le main qui se trouve dans un autre fichier, on initialise la structure avec des API qui retourne des valeurs apparemment suffisamment aléatoires pour que le compilateur ne puisse faire aucune supposition sur elle.
Nous créons le projet avec Visual Studio 2003 (mais toutes ces opérations sont valables avec le 2002 ou 2005)
Nous activons bien, pour la plateforme « Release », les optimisations classiques du Visual C++.
Fenêtre des propriétés du projet
Pour observer le résultat des optimisations, nous demandons la génération des fichiers assembleurs et d'un .map au link.
Demander d'obtenir le code source assembleur généré par le compilateur
Demander d'obtenir un fichier .map
Pour établir notre comparaison, nous allons établir la configuration ReleaseGL en y activant « Whole Program Optimisation ».
Créer une nouvelle configuration
Activation de l'option Whole Program Optimisation
L'option /GL a donc été ajoutée lors de l'appel du compilateur
Résumé des options passées au compilateur
Et l'option « /LTCG » lors de l'appel de l'édition des liens
Résumé des options passées au linker
Au passage, pour gagner un peu de place sur la taille l'exécutable, nous désactivons « Optimize for Windows 98 », qui permet de gagner un plus petit de 8 ko au pris d'une consommation mémoire légèrement plus importante sous Windows 95/98.
Comparons le fichier .map : dans la version « whole optimised », la petite fonction DemoGL_IncL2 n'apparaît pas : elle a été fondue dans la fonction appelante, comme une fonction « inline ». C'est pour cela que nous avons mis autant de calcul étrange dans DemoGL_DispL : elle est devenue trop grosse pour être recopiée en inline à chaque fois qu'elle est utilisée.
Regardons maintenant le cœur du code généré pour le main, et observons tout ce que le compilateur a optimisé :
; 9 : DEMOSTRUCT ds;
; 10 : ds.l1 = GetTickCount();
call
DWORD
PTR
__imp__GetTickCount@0
mov
esi
, eax
mov
DWORD
PTR
_ds$[esp
+
20
], esi
; 11 : ds.l2 = GetVersion();
call
DWORD
PTR
__imp__GetVersion@0
mov
ebx
, eax
; 12 :
; 13 : DemoGL_DispL(&ds);
lea
edi
, DWORD
PTR
_ds$[esp
+
20
]
mov
DWORD
PTR
_ds$[esp
+
24
], ebx
call
?DemoGL_DispL@@YAXPAUDEMOSTRUCT@@@Z ; DemoGL_DispL
; 14 : ds.l1+=2;
add
esi
, 2
mov
DWORD
PTR
_ds$[esp
+
20
], esi
; 15 : DemoGL_DispL(&ds);
call
?DemoGL_DispL@@YAXPAUDEMOSTRUCT@@@Z ; DemoGL_DispL
; 16 : ds.l1+=3;
add
esi
, 3
; 17 :
; 18 : DemoGL_IncL2(&ds);
; 19 : DemoGL_IncL2(&ds);
add
ebx
, 14
; 0000000eH
mov
DWORD
PTR
_ds$[esp
+
20
], esi
mov
DWORD
PTR
_ds$[esp
+
24
], ebx
; 20 : DemoGL_DispL(&ds);
call
?DemoGL_DispL@@YAXPAUDEMOSTRUCT@@@Z ; DemoGL_DispL
pop
edi
pop
esi
Première remarque : avant d'appeler le premier DemoGL_DispL (ligne 13), la valeur de ds.l1 figurait dans un registre (esi), en plus d'avoir été mise dans la structure (pour être utilisée en lecture par DemoGL_DispL). Au retour de la fonction, le compilateur utilise esi pour y trouver la valeur de ds.l1 : l'analyse globale de l'optimisateur lui a permis de savoir que ds.l1 n'était pas modifié par la fonction, et donc que le registre contient toujours la bonne variable.
Seconde remarque : la fonction DemoGL_IncL2 est non seulement fondue dans la fonction appelante, mais réinterprété : ainsi, un double appel à cette fonction qui ajoute 7 à ds.l2 se traduit par un unique « add ebx,14 ». Dans la version sans « whole optimisation » ; chaque appel se traduit par 3 instructions dans la fonction principale, dont un call.
Ces exemples montrent ce que peut apporter cette option à la qualité de l'optimisation. En 64 bits, avec l'augmentation du nombre de registres généraux, les bénéfices peuvent être plus important (connaissance des registres non modifiés par une fonction, adaptation du nombre de paramètres passés par registre, sans tenir compte des normes de type cdecl ou fastcall pour une fonction non exportée…).
N'hésitez pas à l'adopter !
Pour en savoir plus :
- Visual C++ Compiler Options - /GL (Whole Program Optimization)
- Visual C++ Extensibility Reference - WholeProgramOptimization Property
- Un article de Matt Pietrek présentant également, en anglais, le Link-time Code Generation / Whole Program Optimisation : Under the hood : Link-time Code Generation
Et plus loin en Visual Studio 2005 :
- en anglais l'optimisation guidée par profil : Profile-Guided Optimization with Microsoft Visual C++ 2005