mardi 19 septembre 2023

Palonnier MFG Crosswind

C'était ma petite folie de la rentrée : je me suis offert un palonnier MFG Crosswind, fabriqué par une toute petite boite en Croatie, mais qui envoie du pâté. Du solide, tout en métal, à part les pédales qui sont en impression 3D d'excellente qualité, avec un amortisseur optionnel (et donc essentiel). C'est un plaisir de l'utiliser, c'est d'une précision redoutable, et en plus c'est plutôt esthétique.

J'ai choisi les pédales Airbus et les pédales "chasse". Joli, non ?

Seul petit souci, le palonnier se met facilement à glisser sur la moquette, et donc je ne dois pas trop serrer l'amortisseur, sinon c'est tout qui bouge !

samedi 16 septembre 2023

Accolades et std::optional

La nouvelle manière unifiée d'initialiser les valeurs en C++, c'est super. Bim, on met deux accolades, et on a gagné. Vraiment ?

Sur le papier, c'est bath, on peut pas se tromper:

int a = {};
std::string b = {};
std::optional<int> c = {};
MonObjectQuIlEstBeau d = {};

Sauf qu'il a danger ! C'est que lorsque l'on fait un changement de type quelque part, mettons un paramètre de fonction, et que l'on s'attend à ce que le compilo nous indique gentiment tous les endroits à changer, eh bien il est possible en fait que le compilo accepte le changement sans broncher, mais en changeant la sémantique. Et boum ! C'est ce qui m'est arrivé la semaine dernière, heureusement rapidement détecté. Voyez plutôt cet innocent programme, l'on a une fonction f qui prend un optionel, et on l'appelle en initialisant notre optionel sur vide.

#include <optional>

void f(std::optional<int> a);

int main()
{
    f({});
    return 0;
}

Un peu plus tard, l'on change la signature de f:

void f(int a);

Et boum, l'on initialise maintenant a avec la valeur 0, ce qui pourrait bien être complètement faux.

Que faire alors ? Eh bien, utilisons std::nullopt ! Ainsi, notre appel f(std::nullopt) fonctionnera correctement dans le premier cas, et causera une erreur de compilation dans le deuxième.

Conclusion : mangez du {}, mais avec les optionels, préférez std::nullopt !

dimanche 6 août 2023

Noexcept - Ce n'est pas gratuit !

Je parle régulièrement de culte du cargo dans le domaine informatique, où l'on a souvent ses petites marottes que l'on applique sans comprendre. Et j'ai un chef dont la marotte c'est d'exiger l'ajout du mot-clé "noexcept" un peu partout, avec l'espoir d'augmenter les performances.

Et c'est le cas, parfois. Par exemple, une réallocation d'un std::vector après un dépassement de capacité lors de l'ajout d'un élément sera bien plus efficace si le type de l'élément a un move constructor designé comme noexcept, car dans ce cas, l'algorithme de réallocation du vecteur peut utiliser le move. Il faut donc bien se rendre compte qu'au sein du code de std::vector, il y a une condition qui vérifie à la compilation si le type a son move constructor designé ainsi, et décide d'utiliser l'algo efficace ou non. Ce n'est donc pas une optimisation du compilateur, mais bien un choix de code au sein de la bibliothèque standard, et c'est complètement indépendant du niveau d'optimisation, de l'inlining...

Et surtout, noexcept n'est pas gratuit ! Si le compilo ne peut déterminer si effectivement la fonction marquée noexcept ne lance pas d'exception (disons, elle appelle simplement une autre fonction pas inlinée qui n'est pas déclarée noexcept), il lui faut rajouter du code pour pouvoir attraper cette exception et terminer le programme à la place. Voyons ce que dit notre ami Godbolt sur ce tout petit programme:

void f();

void g()
{
    f();
}

L'assembleur généré est celui-là, c'est à dire un bête jump et puis c'est tout !

g():
        jmp     f()

En revanche, si l'on dit maintenant que g() est noexcept:

void f();

void g() noexcept
{
    f();
}

Alors l'assembleur généré nous montre bien que l'on fait un peu plus de travail, en poussant la frame sur la pile, afin que si une exception était lancée dans f(), il soit possible de l'arrêter dans g().

g():
        subq    $8, %rsp
        call    f()
        addq    $8, %rsp
        ret

Conclusion que ne renieraient pas Plic et Ploc : réfléchir avant d'agir !

lundi 31 juillet 2023

Fontes pour programmer

C'est en lisant un article de Slashdot sur une nouvelle fonte mono particulièrement lisible que je me suis mis à reconsidérer mes choix. Jusqu'à présent, j'étais plutôt satisfait des fontes par défaut pour coder dans Emacs, mais de plus en plus, j'étais frustré par la ressemblance du 1 et du l ainsi que du 0 et du O. C'était donc l'occasion d'en changer.

J'ai découvert qu'il existait 2 fontes souvent citée dans ce domaine, Consolas sous Windows et Inconsolata sous Linux. Après quelques tentatives, je confirme que l'essayer, c'est l'adopter.

Regardez-donc Inconsolata: c'est beau, non ?

lundi 10 juillet 2023

Architecture CPU et optimisation du compilo

Et on continue de jouer avec Godbolt, en particulier la capacité de gcc d'optimiser du code numérique. Prenons par exemple une version simplifiée de la résolution d'une équation quadratique :

#include <cmath>

double calc(double a, double b, double c)
{
    double delta = b * b - 4 * a * c;
    return (-b + sqrt(delta)) / (2 * a);
}

Je vous laisserai aller voir sur Godbolt, mais en gros, ça nous fait une 30aine d'instructions assembleur.

La première grosse optimisation, c'est la combinaison de -O3 et de -ffast-math. On passe à 18 lignes (dont 12 seulement font quelque chose d'intéressant), et le compilo utilise l'instruction assembleur sqrtsd plutôt que d'appeler le sqrt de la libc. Mais l'on peut faire encore mieux ! Avec un -march=skylake, on perd 2 instructions, et la machine passe alors sur des instructions vectorisées. Plus rapide ? Moins rapide ? Je suppose que ça va dépendre ! On retrouve un code équivalent en passant chez AMD et -march=znver3.

Maintenant, le souci, c'est que je si, comme dans mon cas, je dois compiler du code qui tournera à la fois sur des machines récentes de chez Intel et AMD, eh bien je suis obligé de garder une architecture générique et de perdre l'avantage de toutes ces instructions magiques... Quand bien même elles sont peut-être en commun ?

Frustrant, donc. Mais probablement pas catastrophique, car le code que j'optimise n'est de toutes façons pas très numérique, et se vectorise probablement mal. Tant pis !

jeudi 8 juin 2023

Gitversion prend ses aises, partie 2

Juste un rebond sur mon fil précédent : je suis allé voir ce qu'il en était de git lui même. Et bien, je n'ai pas été déçu : avec grosso modo une dépendance sur la libc, sur zlib, et sur les regex perl, git tient dans à peine plus que 3 megs. Il fait donc à peu près 100 fois plus que gitversion, en 20 fois moins de place. Yay !

mardi 6 juin 2023

Gitversion prend ses aises

Gitversion est un outil ma foi plutôt malin qui regarde un dépôt git et décide du prochain numéro de version, en se basant sur une série de règles, typiquement le tag précédent et certains mots-clé dans les messages de commit. Comme l'outil, ou du moins sa version en ligne de commande (parce qu'il est également disponible en intégration continue par exemple) est plutôt simple, je me suis demandé quelle était la techno derrière.

Un petit ldd plus tard, premier bon point : en fait de dépendances, c'est minimal, en gros libstdc++. Je regarde donc le binaire en lui même, c'est du bon vieux elf, mais les chaines présentes dedans contiennent beaucoup de références à Microsoft. Mhh... Allons donc voir le source.

Et en effet, c'est du C#. Beaucoup de C#, d'ailleurs. Pris d'un doute, je regarde donc la taille du binaire... 66 megs !

Les types ont donc embarqué un interpreteur mono plus tout une palanquée de bibliothèques .NET pour lire quelques lignes de log et générer 3 malheureux nombres qui se battent en duel...

Portabilité, que de disques on a rempli en ton nom !