En complément et histoire de donner des arguments simples mais percutants, voilà un petit test que je viens de faire sur la carte sur laquelle je bosse (STM32F746VGT6) :
int testC(uint8_t value)
{
uint32_t start = DWT->CYCCNT;
uint32_t buffer[333];
uint32_t i;
for (i = 0; i < 333; i++)
buffer = value;
return DWT->CYCCNT - start;
}
int testCpp(uint8_t value)
{
auto start = DWT->CYCCNT;
std::array<uint8_t, 333> buffer;
buffer.fill(value);
return DWT->CYCCNT - start;
}
bool testCvsCpp(uint8_t value)
{
auto c = testC(value);
auto cpp = testCpp(value);
return c > cpp;
}
Il s'agit tout simplement de déclarer un tableau et de le remplir d'une valeur fixe. Je suis resté en O0, mais le résultat en O3 serait encore pire ...
La première et dernière ligne ne sont là que pour compter le nombre de cycles d'horloge passées dans la fonction, si on les enlève:
- en C on a 4 lignes dont la signification ne saute pas de suite aux yeux (avec de l'habitude on comprend vite), on a une constante répétée (la taille du tableau), et au moins une dizaine de bugs prêts à vous sauter à la gorge.
- en C++ on a 2 lignes de code, la constante n'est pas répétée, la signification est limpide (la fonction "fill" est très claire, rempli le tableau), et il est compliqué de se tromper ou de faire un dépassement (c'est le compilateur qui s'en occupe, plus le codeur)
Et maintenant la grande révélation ... le nombre de cycles d'horloge (sans trucage, si vous avez une nucleo vous pouvez refaire le test chez vous, j'ai utilisé Atollic dernière version, donc compilateur GCC assez récent).
- en C : 6421 cycles d'horloge
- en C++ : 1101 cycles d'horloge, quasiment 6x plus rapide !!!!!!!!
Alors évidemment ça ne tombe pas du ciel, pourquoi le C++ peut être aussi rapide ? Tout simplement parce qu'on laisse plus de latitude au compilateur pour optimiser en ne rajoutant pas de contrainte particulière, je m'explique:
- en C on a demandé spécifiquement au compilateur de remplir le tableau octet par octet
- en C++ on lui a simplement demandé de remplir le tableau, la différence parait infime, mais ici le compilateur a eu la liberté de copier la valeur par paquet de 4 octets (on est sur une plateforme 32 bits), et probablement d'utiliser du store/load multiple, qui est la solution la plus rapide pour copier de la mémoire avec le Cortex-M (et pourtant je n'ai pas pris un taille de tableau en multiple de 4, pour ne pas l'avantager encore plus).
Il faudrait par exemple en C utiliser "memset" qui lui peut optimiser davantage, mais il faut connaitre (en C++ il suffit de faire "buffer." et de laisser l'IDE vous proposer les méthodes accessibles), et la syntaxe (pointeur) laisse encore plus de possibilités de mettre des bugs partout.
Et pour enfoncer le clou, sur un calcul de ce type, il suffirait de mettre la fonction C++ en constexpr (si les entrées sont connues au moment de la compilation, ce qui est le cas ici si la variable "value" est une valeur fixe dans le code, et le résultat prendrait seulement quelques cycles d'horloge, puisque c'est le compilateur lui-même qui ferait le calcul à la compilation, et non plus à l'exécution !
Bref il y a beaucoup de choses à dire sur le sujet.
Thomas.
EDIT : Détail intéressant, je viens de regarder sur godbolt.org les deux version en compilation O3, et le compilateur est suffisamment intelligent pour comprendre que la version C est un remplissage de tableau, et le remplacer par un appel à memset, ce qui donne exactement le même résultat que la version C++. Mais on se base là sur une optimisation du compilateur qui n'aura pas toujours lieu (il faut vraiment que le pattern soit reconnu par le compilateur), alors que la version C++ est toujours optimizable (y compris en O0).