Profiling
Cet outil permet d'obtenir un certain nombre de mesures sur
l'exécution d'un programme : nombre de passages dans une fonction ou
dans certaines structures de contrôle (conditionnelles, filtrages et
boucles). Les informations sont enregistrées dans un fichier. De
leur examen on peut déduire des erreurs algorithmiques ou les points
cruciaux à optimiser dans une application.
Pour pouvoir traiter le code, il est nécessaire de compiler
l'exécutable dans un mode spécial qui lui ajoute les instructions
nécessaires aux divers décomptes. Il existe deux modes de
profiling, un pour le compilateur de code-octet et un autre pour le
compilateur natif, ainsi que deux commandes pour la présentation des
résultats. Dans l'analyse du code natif on récupère
aussi le temps passé dans chaque fonction.
Le profiling d'une application se déroule donc en trois temps :
-
compilation en mode profiling;
- exécution du programme;
- présentation des mesures.
Commandes de compilation
Les commandes de compilation en mode profiling sont les
suivantes :
-
ocamlcp -p options
pour le compilateur de code-octet ;
- ocamlopt -p options pour le compilateur natif.
Ces compilateurs produisent le même type de fichiers que les commandes
habituelles (7). Les différentes options
sont décrites à la figure 10.1.
f |
appel de fonctions |
i |
branches du if |
l |
boucles while et for |
m |
branches du match |
t |
branches du try |
a |
toutes les options |
Figure 10.1 : Options des commandes de profiling
Elles indiquent quelles structures de contrôle doivent être prises en
compte. Par défaut les options fm sont activées.
Exécution d'un programme
Compilateur de code-octet
L'exécution d'un programme compilé dans un mode de profilage produira,
si il termine, un fichier nommé ocamlprof.dump qui contient les
différentes informations demandées.
On reprend l'exemple sur le produit des entiers d'une liste. On écrit
le fichier f1.ml suivant :
let
rec
interval
a
b
=
if
b
<
a
then
[]
else
a::(interval
(a+
1
)
b);;
exception
Found_zero
;;
let
mult_list
l
=
let
rec
mult_rec
l
=
match
l
with
[]
->
1
|
0
::_
->
raise
Found_zero
|
n::x
->
n
*
(mult_rec
x)
in
try
mult_rec
l
with
Found_zero
->
0
;;
et le fichier f2.ml qui utilise les fonctions de f1.ml :
let
l1
=
F1.interval
1
3
0
;;
let
l2
=
F1.interval
3
1
6
0
;;
let
l3
=
l1
@
(0
::l2);;
print_int
(F1.mult_list
l1);;
print_newline();;
print_int
(F1.mult_list
l3);;
print_newline();;
La compilation de ces fichiers en mode profilage est la suivante :
ocamlcp -i -p a -c f1.ml
val profile_f1_ : int array
val interval : int -> int -> int list
exception Found_zero
val mult_list : int list -> int
Avec l'option -p
, le compilateur ajoute une nouvelle fonction
(profile_f1_) pour l'initialisation des compteurs du module
F1. Il en est de même pour le fichier f2.ml :
ocamlcp -i -p a -o f2.exe f1.cmo f2.ml
val profile_f2_ : int array
val l1 : int list
val l2 : int list
val l3 : int list
Compilateur natif
La compilation en code natif donne le résultat suivant :
$ ocamlopt -i -p -c f1.ml
val interval : int -> int -> int list
exception Found_zero
val mult_list : int list -> int
$ ocamlopt -i -p -o f2nat.exe f1.cmx f2.ml
Seule l'option -p sans argument est utilisée. L'exécution de
f2nat.exe produit un fichier de nom
gmon.out. Celui-ci possède un format traitable par les
outils habituels Unix (voir page ??).
Présentation des résultats
Comme les informations recueillies par les deux modes de
profiling sont différentes, leur présentation s'en ressent. Dans le
premier mode (code-octet), des commentaires sur le nombre de
passages dans les structures de contrôle sont ajoutés au texte du
programme. Dans le second (natif), on associe à chaque fonction le
temps passé dans son corps et son nombre d'appels.
Compilateur code-octet
La commande ocamlprof donne l'analyse des résultats des
mesures. Elle utilise les informations contenues dans le fichier
camlprof.dump. Cette commande prend en entrée le source du
programme, lit le fichier de mesure et produit un nouveau texte de
programme contenant en commentaires les compteurs demandés.
Sur notre exemple cela donnera :
$ ocamlprof f1.ml
let rec interval a b =
(* 62 *) if b < a then (* 2 *) []
else (* 60 *) a::(interval (a+1) b);;
exception Found_zero ;;
let mult_list l =
(* 2 *) let rec mult_rec l = (* 62 *) match l with
[] -> (* 1 *) 1
| 0::_ -> (* 1 *) raise Found_zero
| n::x -> (* 60 *) n * (mult_rec x)
in
try mult_rec l with Found_zero -> (* 1 *) 0
;;
Ces compteurs reflètent bien les calculs demandés dans F2. Il
y a deux appels à list_mult et 62 à la fonction auxiliaire
mult_rec. L'exploration des branches du filtrage
match montre les 60 passages dans le cas courant, l'unique
passage dans le filtre [] au premier calcul, et l'unique passage
dans le filtre de tête égal à 0, déclenchant une exception dont le
filtrage se voit dans le compteur du try.
La commande camlprof accepte deux options. La première -f fichier indique le nom du fichier contenant les mesures. La
seconde -F chaîne spécifie une chaîne à ajouter
dans les commentaires associés aux structures de contrôle traitées.
Compilateur natif
Pour connaître le temps passé dans les appels des fonctions de
multiplication des éléments d'une liste, on écrit le fichier
f3.ml suivant :
let
l1
=
F1.interval
1
3
0
;;
let
l2
=
F1.interval
3
1
6
0
;;
let
l3
=
l1
@
(0
::l2);;
for
i=
0
to
1
0
0
0
0
0
do
F1.mult_list
l1;
F1.mult_list
l3
done;;
print_int
(F1.mult_list
l1);;
print_newline();;
print_int
(F1.mult_list
l3);;
print_newline();;
C'est le même fichier que f2.ml avec une boucle de 100000
itérations.
L'exécution du programme crée le fichier gmon.out.
Celui-ci est dans un format lisible par la commande gprof que
l'on trouve sur les systèmes Unix. L'appel suivant de gprof
affiche les informations de temps passé et le graphe des appels. Comme
la sortie est assez longue, on ne montre que la première page qui
contient le nom des fonctions appelées au moins une fois et le temps
passé dans chacune d'elles.
$ gprof f3nat.exe
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
92.31 0.36 0.36 200004 1.80 1.80 F1_mult_rec_45
7.69 0.39 0.03 200004 0.15 1.95 F1_mult_list_43
0.00 0.39 0.00 2690 0.00 0.00 oldify
0.00 0.39 0.00 302 0.00 0.00 darken
0.00 0.39 0.00 188 0.00 0.00 gc_message
0.00 0.39 0.00 174 0.00 0.00 aligned_malloc
0.00 0.39 0.00 173 0.00 0.00 alloc_shr
0.00 0.39 0.00 173 0.00 0.00 fl_allocate
0.00 0.39 0.00 34 0.00 0.00 caml_alloc3
0.00 0.39 0.00 30 0.00 0.00 caml_call_gc
0.00 0.39 0.00 30 0.00 0.00 garbage_collection
...
Le premier enseignement est que presque tout le temps d'exécution se
déroule dans la fonction F1_mult_rec_45, qui
correspond à la fonction F1.mult_rec du fichier f1.ml.
On s'aperçoit d'autre part que de nombreuses fonctions sont
appelées. Les premières de la liste sont les fonctions de la
bibliothèque d'exécution de gestion mémoire (9).