Les bienfaits de l'option Whole Program Optimisation de Visual C++ .Net

Gilles Vollant

Avez-vous remarqué l'option d'optimisation baptisée Optimisation de l'ensemble du programme (Whole Program Optimisation en anglais) disponible depuis Visual C++ 2002 ? Si ce n'est pas le cas, alors découvrez dans cet article comment bénéficier sans effort de meilleures performances dans vos programmes. Et si vous êtes déjà un initié, votre curiosité sera satisfaite grâce à une étude claire et détaillée du mécanisme interne de cette option ainsi que son impact dans vos projets. ♪

- Titre : Les bienfaits de l'option Whole Program Optimisation de Visual C++ .Net

- Auteur : Gilles Vollant

- Parution : 25 avril 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Gilles Vollant. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

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.

Fichier democgl.h

/* democgl.h */

typedef struct

{
long l1;
long l2;
} DEMOSTRUCT;

void DemoGL_DispL(DEMOSTRUCT* pds);

void DemoGL_IncL2(DEMOSTRUCT* pds);

Fichier demofnc.cpp

/* 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;
}

Fichier democgl.cpp

/* 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