Benchmark CPU sur Amazon EC2

Cet article constitue un retour d’expérience sur un benchmark CPU sur différents types (tailles) d’instances EC2 sur AWS. L’objectif était de constater le comportement, au niveau des ressources CPU, desdites instances lors d’une montée en charge sur un traitement multi-threadé et de les comparer par rapport à un étalon plus récent (choisi arbitrairement, comme un portable) que celui proposé par AWS : l’ECU ou EC2 Compute Unit.
Tout d’abord, je tiens à remercier Sylvain Terret qui a effectué le test « en ressortant un vieux bout de code du placard » et qui a aussi écrit un billet sur son blog sur le sujet. Ensuite, je fais également écho à un article intéressant (EC2 isn’t 50% slower) qui répond à quelques controverses sur la réalité des ressources CPU mises à disposition lors du lancement d’une instance EC2. Je vous invite à lire cet article synthétique et instructif, ainsi que les commentaires associés.
Pour commencer, le bench a été effectué sur un Ubuntu Lucid Lynx. Le code ci-dessous a été utilisé pour charger les différents types d’instances EC2 : il s’agit d’une multiplication de matrices basée sur l’API OpenMP.
Ce calcul matriciel va être exécuté plusieurs fois en incrémentant à chaque fois le nombre de threads qui vont se partager (parallélisation) le calcul. Ainsi, nous avons pu comparer un même traitement lorsqu’il est exécuté par 1, 2, … 12 threads et constater l’évolution du temps d’exécution en fonction du type d’instance EC2, c’est à dire, dans le cas qui nous intéresse, en fonction du nombre de CPUs disponibles sur chaque type d’instance. Il est à noter que chaque CPU affecté au traitement d’un thread donné est monopolisé à 100% lors de l’exécution du calcul.
Le code source est le suivant :
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#include <sys/time.h>#define SIZE 2000
double get_time() {
struct timeval tv;
gettimeofday(&tv, (void *)0);
return (double) tv.tv_sec + tv.tv_usec*1e-6;
}int main(int argc, char **argv){
int nb, i , j, k;
double t,start,stop;
double* matrice_A;
double* matrice_B;
double* matrice_res;
matrice_A = (double*) malloc(SIZE*SIZE*sizeof(double)) ;
matrice_B = (double*) malloc(SIZE*SIZE*sizeof(double)) ;
matrice_res = (double*) malloc(SIZE*SIZE*sizeof(double)) ;
for(i = 0; i < SIZE; i++){
for(j = 0; j < SIZE; j++){
matrice_A[i*SIZE + j] = (double)rand()/(double)RAND_MAX;
matrice_B[i*SIZE + j] = (double)rand()/(double)RAND_MAX;
}
}
printf(« Nb.threads\tTps.\n »);
for(nb=1;nb<=12;nb++){
start = get_time();
#pragma omp parallel for num_threads(nb) private(j,k)
for(i = 0; i < SIZE; i++){
for(j = 0; j < SIZE; j++){
matrice_res[i*SIZE + j] = 0.0;
for(k = 0; k < SIZE; k++){
matrice_res[i*SIZE + j] += (matrice_A[i*SIZE + k]*matrice_B[k*SIZE + j]);
}
}
}
stop=get_time();
t=stop-start;
printf(« %d\t%f\n »,nb,t);
}
free(matrice_A);
free(matrice_B);
free(matrice_res);
return EXIT_SUCCESS;
}
Les instances benchées sont les suivantes :
- Standard Instances – Large Instance (M1.LARGE) 7.5 GB of memory, 4 EC2 Compute Units (2 virtual cores with 2 EC2 Compute Units each), 850 GB of local instance storage, 64-bit platform
- Standard Instances – Extra Large Instance (M1.XLARGE) 15 GB of memory, 8 EC2 Compute Units (4 virtual cores with 2 EC2 Compute Units each), 1690 GB of local instance storage, 64-bit platform
- High-CPU Instances – High-CPU Extra Large Instance (C1.XLARGE) 7 GB of memory, 20 EC2 Compute Units (8 virtual cores with 2.5 EC2 Compute Units each), 1690 GB of local instance storage, 64-bit platform
A noter, la définition de l’ECU (rien à voir avec une nouvelle-ancienne monnaie unique) qui est l’étalon AWS en termes de puissance CPU et qui correspond à :
EC2 Compute Unit (ECU) – One EC2 Compute Unit (ECU) provides the equivalent CPU capacity of a 1.0-1.2 GHz 2007 Opteron or 2007 Xeon processor.
Cela, disons-le, ne représente pas grand chose, mais j’y reviendrai plus tard.
A noter également notre étalon qui est constitué d’un portable posé sur le bureau et équipé d’un Core 2 Quad Q8400 @ 2.66 GHz x 1 (4 cores).
Synthèse graphique

CPU benchmark on EC2
Analyse des résultats
- Tout d’abord de manière macro, il est rassurant de voir que doubler le nombre de « virtual cores » (2 => 4 => 8) double les performances pour des « virtual cores » sensiblement identiques (2 virtual cores de 2 ECUs chacun => 4 virtual cores de 2 ECUs chacun => 8 virtual cores de 2.5 ECUs chacun). Comme le dit Sylvain : « Ca sent un peu le Captain Obvious, mais à bien y réfléchir, ce n’est pas une règle d’or dans l’informatique. »
- Le Core 2 Quad posé sur le bureau est plus efficace à nombre de coeurs équivalent que les instances EC2 (une M1.XLARGE avec 4 coeurs de 2 ECUs chacun dans notre cas).
- Ensuite, il est intéressant de noter que nous n’avons obtenu sur le lancement des instances que des processeurs Xeon (Intel) : E5430 @ 2.66 GHz pour les M1.LARGE et M1.XLARGE et E5410 @ 2.33 GHz pour les C1.XLARGE. Quelles auraient été les résultats si nous avions eu des Opteron (AMD) ? Il faut savoir que l’on ne choisit pas son type de processeur au lancement d’une instance. Cependant, cela fait quelques temps que je n’ai plus vu d’Opteron au lancement d’instances EC2 : il faut dire que je travaille depuis quelques temps quasi exclusivement sur la région européenne d’Amazon (par opposition aux EU, voire à l’APAC). Je n’ai pas investigué sur ce sujet beaucoup plus au delà.
- Une anomalie est à noter concernant les tests sur les M1.XLARGE : la différence de performance sur les 3 premiers calculs (de 1 à 3 threads) entre les deux instances M1.XLARGE (4 virtual cores) : une des 2 instances a donné des performances moins bonnes sur le même bench qui a tourné deux fois sur chaque machine (les résultats ont été constants). Je n’ai pas trouvé d’explication sur cette différence entre les 2 instances.
- Ce qui n’apparaît pas sur le graphique et que l’on constate, en se connectant directement sur les instances, via top ou htop : on voit à chaque nouveau calcul exécuté et distribué sur un thread supplémentaire un nouveau CPU passer à 100% (calcul sur 2 threads = 2 CPUs à 100%, …) et lorsque l’on atteint « nombre de threads >= nombre de CPUs de l’instance », on voit apparaître un pourcentage de steal sur les CPUs de l’instance.
Le % steal
Pour analyser un peu plus en détail la présence de steal, nous avons effectué un autre test de performance (merci à Yann Le Van pour le graphe !) en monitorant un MongoDB (toujours sur Ubuntu Lucid Lynx) que nous avons maltraité en y injectant par palier de nombreuses requêtes de géolocalisation non bornées avec des critères de recherche sur des attributs supplémentaires (Cf. requêtes géospatiales et indexation composée). Il apparaît de nouveau clairement qu’un pourcentage non négligeable de steal progresse avec l’utilisation des CPUs de l’instance (les 8 coeurs d’une C1.XLARGE dans ce test).

CPU Benchmark MongoDB

CPU Benchmark MongoDB - Legende
Il ne s’agit pas de puissance CPU que l’on nous dérobe honteusement (ce que j’ai lu dans plusieurs articles concernant ce sujet), mais d’une puissance de calcul utilisé par l’hyperviseur qui gère la virtualisation sur laquelle repose notre OS. Ce test le prouve car le % steal progresse (ou décroit) proportionnellement à notre propre sollicitation de l’instance. Si il s’agissait de la sollicitation de « notre » puissance CPU par une autre instance d’un autre utilisateur sur le même hardware, l’évolution de la valeur du % steal ne serait pas directement reliée à notre utilisation (elle pourrait être constante par exemple).
La valeur du % steal n’est cependant pas négligeable et l’hyperviseur est quelque peu gourmand sur une sollicitation importante des ressources CPU de notre instance.
Conclusion
Comme je disais précédemment concernant l’étalon Amazon, l’ECU, il donne une idée de la puissance comparative des instances EC2 entre elles : si vous faites fonctionner une application multi-threadée sur une instances EC2 et que vous avez besoin de plus de puissance CPU, prendre une instance 2 fois plus puissante en termes d’ECUs vous permettra d’aller 2 fois plus vite (à noter qu’en fonction du type d’instance, un coeur à une puissance de 2 à 3.25 ECUs, si on fait abstraction de la Small Instance et de la Micro Instance qui sont moins puissantes).
Sorti du paradigme AWS, l’ECU ne signifie plus rien car la valeur du GHz dépend de l’architecture, de la génération et du fabricant (Intel, AMD, …) du processeur. Un « 1.0-1.2 GHz 2007 Opteron or 2007 Xeon » ne vous donnera pas une indication très précise sur la puissance réelle dont vous allez disposer, à part le fait qu’à GHz égal, votre ordinateur de bureau équipé de la dernière génération de processeurs vous donnera de meilleurs résultats.
La meilleure (et probablement la seule valable) solution afin de constituer votre choix d’instances à déployer pour votre application est de mettre en place quelques tests de performances afin de constater réellement les ressources utilisées dans le paradigme AWS et de sélectionner les instances et de choisir leur nombre (scale-out) en fonction du résultat obtenu.


Bonjour,
Merci pour l’info …
Rémy
Tiens, en repassant sur l’article (on a toujours besoin de faire un petit test CPU ! :o)) je viens de m’apercevoir que les #include ont sauté dans la mise en page WordPress :
ainsi que les tabulations pour une meilleure lisibilité du code… Et puis quelques caractères qui ne sont pas passés… Ca ne risque pas de compiler ! :o)
Je corrige cela de suite ! :o)
Et pour information, la ligne pour la compilation du programme avant exécution :