Les bienfaits de l’option « Whole Program Optimisation » de Visual Studio .Net

 

Par Gilles Vollant

info@winimage.com

 

Depuis Visual Studio 2002, le compilateur du Visual C++ comporte une option d’optimisation pouvant produire des résultats très intéressant : l’option« Whole Program Optimisation ».

 

En activant cette option, le compilateur n’utilise plus la séparation des tâches en deux temps bien distinct : la compilation séparée de chaque source C++ (ou C) en un fichier .obj par cl.exe, puis le liens 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 distribué 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 distribué des EXE ou des DLL (y compris bien sur 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"

 

void 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);

}

 

 

Evidemment, 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 superflus 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éatoire 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++

 

Pour observer le résultats des optimisations, nous demandons la générations des fichiers assembleurs et d’un .map au link

 

 

 

Pour établir notre comparaison, nous allons établir la configuration ReleaseGL en y activant « Whole Program Optimisation ».

 

 

 

L’option /GL a donc été ajoutée lors de l’appel du compilateur

 

 

Et l’option « /LTCG » lors de l’appel de l’édition des liens.

 

 

Au passage, pour gagner un peu de place sur la taille l’exécutable, nous désactivons « Optimize for Windows 98 », qui permet d’avoir 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é fondu 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 devenu 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 principal, 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 norme de type cdecl ou fastcall pour une fonction non exportée…)

N’hésitez pas à l’adopter !

 

 

Pour en savoir plus :

http://msdn.microsoft.com/library/en-us/vccore/html/vcgrfglwholeprogramoptimization.asp

http://msdn.microsoft.com/library/en-us/vcext/html/vxlrfWholeProgramOptimizationProperty.asp

 

 

Un article de Matt Pietrek présentant également, en anglais, le Link-time Code Generation / Whole Program Optimisation :

http://msdn.microsoft.com/msdnmag/issues/02/05/Hood/

 

Et plus loin en Visual Studio 2005 :

En français, sur les Optimisation guidée par profil : http://www.microsoft.com/france/msdn/technologies/outils/visualc/info/2004-06-02-profileguidedoptimization.html

http://msdn.microsoft.com/library/en-us/dv_vstechart/html/profileguidedoptimization.asp (même article en anglais).