Les bienfaits de l'option "whole program optimisation" du compilateur c++ de visual studio 2002 et supérieur

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 coeur 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'adapter !

Pour en savoir plus, 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/

J'ai également écrit un tutoriel pour automatiser l'utilisateur de cette nouvelle optimisation.

A voir également
Ce document intitulé « Les bienfaits de l'option "whole program optimisation" du compilateur c++ de visual studio 2002 et supérieur » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous