mardi 16 juillet 2019

 

  Soliloqueries et Marmonades à propos de

Division du travail IT & DevOps


Même pas accoudé au comptoir

Quand sur mon fil facebook est apparu un lien vers un article de blog.
 Division du travail IT & DevOps qui plus est avec appel à commentaires.

Franchement, c'est bien parce que je n'ai rien de mieux à faire comme Chief Sleeping Officer chez Gland d'Atome, que je me retrouve devant mon clavier.

Rétro

Rétroviseur, hein. Pas rétro comme dans Scrum. Ou alors après une itération de 40 ans. Est-ce qu'on peut faire des sprints de 40 ans en Scrum?
  • Divisions de 1978 (SSII de 500 personnes):
    • Analyste
    • Analyste-Programmeur
    • Programmeur
    • Perforatrice.
    • Perfo-vérif
    • Opérateur
    • Secrétaire
Ce n'est pas exhaustif. J'ai laissé de côté les aspects management. 

Un "mini-ordinateur" pour une équipe d'une vingtaine de personnes. On écrivait nos programmes sur des feuilles de programmation, qu'on donnait à des perforatrices qui nous redonnait un paquet de cartes perforées quelques jours après. La compilation se faisait la nuit sous la responsabilité de l'opérateur. La doc était manuscrite et dactylographiée par une secrétaire.


Dans le temps, plus maintenant...
Parce que maintenant, mon Dieu, Seigneur !
Le monde a bien changé, sais-tu
  (Julos Beaucarne, Périclès)
  •  Divisions en1989 et en 2002  (PME de 25 à 80 personnes)
    • Chef de projet
    • Architecte
    • Programmeur
    • Testeur
    • Support
    • Documentation
    • Formation
    • Avant-vente
    • Marketing
    • Recherche
    • Support IT
Alors que sur la division de 1978, on avait une fonction une personne, dans la PME de 25 personnes, un individu pouvait remplir toutes ces fonctions.
  • Divisions de 1997 (Groupe Mondial, 40 000 personnes)
    • La même chose que dans la PME
Sauf que. On retrouve la répartition, une personne une fonction. Si t'es architecte, tu architectes. Plus besoin de coder, plus besoin de voir des clients.
  • Divisions de 2010 à 2018 (PMEs de 150 personnes)
    • Étrangement  même affectation personne / fonction que dans le groupe de 40 000 personnes.
Curieusement dans la dernière entreprise de 150 personnes, j'avais autant d'échelons hiérarchiques (4) au dessus de moi que dans le groupe de 40 000 personnes. En dessous aussi d'ailleurs à savoir zéro!

Dans un groupe de 40 000, il est difficile de passer d'une fonction à une autre. Ça perturbe violemment les DRH. Les DRH tiennent à ce que "tu" progresses. Si après avoir piloté une équipe, tu veux retourner à la technique, le DRH ne peut pas comprendre. L'amélioration individuelle pour un DRH (de l'époque) passait par plus de spécialisation. L'amélioration par la diversité et une vision globale ne devait pas être mentionnée dans leurs tablettes RH.


Dans les situations où l'affectation se fait sur la base une personne / une fonction, on a plus de situations de crispation (en clair les équipes se fritent) que dans la situation où une personne a divers rôles.

Gestion par Objectifs et KPI


Est-ce que la taille critique se trouve entre 80 et 150 personnes ?

Ça peut jouer. Mais je pense que la clé est ailleurs.

Quand une entreprise veut être organisée comme une grande (la start-up est "ready to scale" pour rassurer les investisseurs, ou même s'organise pour projeter une ombre plus grande que la réalité, ça rappelle la grenouille de La Fontaine)...

Donc, quand une entreprise veut être organisée comme une grande, elle met en place des objectifs et des outils de mesure pour suivre l’efficacité des équipes. A charge pour le chef d'équipe de présenter des bons indicateurs (Key Performance Indicators) régulièrement.

Donc même si on n'a pas un management par objectifs au sens strict, l'effet est le même. Chaque équipe se retrouve avec un intérêt particulier qui ne prend pas en compte l'intérêt des autres équipes et pas celui de la communauté (l'entreprise).

Résultat, même avec de la bonne volonté, la coopération entre équipes est limitée par l'intérêt des équipes défini par les KPI de chacune de ces équipes.

C'est évidemment très simplifié.

De l'agilité

Avant 2010, la gestion était plutôt classique genre Cycle en V informel. 
Ensuite, j'ai goûté à Scrum, j'ai été initié à Kanban sans pratiquer.
Et j'ai fini par du AINO.(Agile In Name Only).
 
De ce que j'ai aperçu de Scrum, il semble que l'idée de laisser les développeurs s'auto-organiser, conduit surtout à leur mettre beaucoup plus de pression psychologique que dans une gestion en V avec Gantt chart. Les sprints de 2 ou 3 semaines avaient l'air d'épuiser les moins robustes (les plus jeunes?). Par contre, le supérieur hiérarchique semblait plus épanoui.

A la réflexion, l'auto-organisation est limitée à "comment s'organiser pour faire au mieux les tâches à accomplir". Les contraintes qui s'appliquent sur l'équipe ne sont pas négociables (ou à la marge). Et l'équipe doit montrer que ses KPIs s'améliorent.

Ah, le burn-rate de l'équipe sur l'écran géant dans l'open-space, et/ou à côté de la machine à café.  On te collerait un Jiminy Criquet dans l'oreille ça ne serait pas pire.

On en revient à l'élément précédent: la direction d'entreprise fixe un découpage et des objectifs à chacune des boites ainsi découpées. Laisser les gens libres dans la cage en bas est sympathique (plus sympathique que de les forcer à adopter une organisation donnée), mais ne peut être que très limité.

Du Dev et du DevOps

Dev... Hum. 

Au fil des années, le développement s'est fortement teinté d'intégration.

Dans mes pérégrinations, la partie développement a toujours été traitée raisonnablement sérieusement. Par contre, les critères de choix pour s'appuyer sur telle ou telle brique existante à intégrer m'ont toujours laissé rêveur.

J'ai sévi essentiellement chez des éditeurs de logiciel. Avec des clients qui pouvaient mettre plusieurs mois à déployer une nouvelle version, ou plus récemment avec des clients plus réactifs.

Prenons un exemple: un logiciel développé suffisamment complexe, pour qu'une équipe soit en charge d'aider les clients à installer, maintenir, opérer le logiciel. Donc une fonction "Ops" ou proche de "Ops".

La communication entre Ops et Dev était raisonnable pour le support. Quand un bug était suffisamment sérieux, il devenait visible de la direction et donc les coopérations jouaient. Quand un bug est bloquant, le client menace le CEO de la boîte qui fournit le logiciel, et les problèmes se résolvent. C'est vrai dans les boites de 150 personnes et dans celles de 40 000.

Pour la partie "facilité d'installation et d'opération". C'est plus compliqué. La première version du logiciel est conçue sans les équipes "ops". Les développeurs n'ont aucune idée (ou une idée simplifiée, naïve) de la complexité de ce qu'ils sont en train de concevoir (du point de vue opérationnel). Quand il s'agit de produire une nouvelle version, le point de vue "opérationnel" même s'il est exprimé et entendu a peu de chance de faire son chemin, si le marketing produit décide que la feature superX est celle qui doit être implémentée au plus vite.

Idée intéressante mais sans effet: faire que chaque développeur passe 2 semaines avec un opérationnel, pour revenir convaincu de la nécessité de prendre en compte le côté opérationnel. Bon. 3 développeurs sur une quarantaine ont suivi ce cursus. Flop. Lâcher un développeur 3 semaines n'a pas d'incidence positive sur les KPIs de l'équipe, ni sur le parcours ultérieur de ce développeur. Reste l'envie personnelle et déterminée de quelques développeurs.


Autre idée ingénieuse (?!): créer une équipe DevOps entre l'équipe Ops et l'équipe Dev. Du DevOps In Name Only en quelque sorte.

Là encore, ça ne s'est amélioré que quand la décision est venue du haut. J'imagine que d'une manière où d'une autre les Ops ont fini par montrer des mauvais KPI. Pour les redresser, il fallait prendre en compte le point de vue "Ops" du côté Dev.

Agrégation de DevOps?

Comment vont faire les entreprises qui agglutinent les services fournis par d'autres au sein de leur offre?

Soit une entreprise qui permet à des artisans de créer en quelques clics leur site web. Puis de pouvoir prendre des commandes et de traiter les paiements associés, puis de pouvoir expédier les commandes par un service de livraison, etc...

Dans cet exemple, on aurait donc un service rendu par une entreprise qui s'appuierait sur un service de Cloud, et qui dialoguerait avec des services bancaires et des services d'expéditions eux-même hébergés sur des Clouds.

Le bon fonctionnement du service suppose que 3 hébergeurs de Clouds et 3 fournisseurs de services fonctionnent sans problème. :-)

Les SLA peuvent partiellement répondre. Ça semble cependant plus complexe. Voir (Nines are not enough: meaningful metrics for clouds).

Mais surtout à la question "qui comprend le système de bout en bout ?",  j'ai peur de malheureusement connaître la réponse.



D'autres pistes

Côté méthodologie, j'ai pendant un temps regardé les travaux de "SEMAT". C'était très embryonnaire quand je m'y suis intéressé. 

Chez Gland d'Atome la méthodologie pour faire la sieste est assez éprouvée et les rétros de fin de sieste ne montrent pas de nécessité de faire évoluer cette méthodologie. Du coup, je n'ai pas suivi les travaux de SEMAT récemment.

Ensuite, il y a des boîtes qui prennent le problème de l'organisation à bras le corps. Comme Semco: 
 Sinon, on m'a recommandé:

HTH

LeFA






vendredi 23 février 2018

La pie et les pingouins

 

 La Pie et les Pingouins

Merci à Alice, Arthur et Yurug  pour le titre.

Ne vous méprenez pas, il n'est pas question que je me prenne pour "un éditeur affable comme il y en a depuis Esope" (Bobby Lapointe, évidemment).

Tout espoir de trouver là des histoires de volatiles, s'est envolé.

Ma plume, j'en ai peur, n'est point duveteuse.


Programme inutile

J'adore les programmes inutiles.

fa$ cat unepie.c
#include


int main(int argc, char **argv)

    printf("Y'a une pie dans le poirier,\n"); 
    printf("J'entends la pie qui chante...\n");
    return 0; 

fa$

Prenons, 2 distributions Linux

Ubuntu 16.4Debian 9.1
fa$ lsb_release -a
Distributor ID:Ubuntu
Description:   Ubuntu 16.04.2 LTS
Release:       16.04
Codename:      xenial

fa$
fa$ lsb_release -a
Distributor ID: Debian
Description:    Debian GNU/Linux 9.1 (stretch)
Release:        9.1
Codename:       stretch

fa$

Compilons, exécutons notre programme.

Ubuntu 16.4Debian 9.1
fa$ gcc unepie.c -o unepie
fa$ ./unepie
Y'a une pie dans le poirier,
J'entends la pie qui chante...
fa$
fa$ gcc unepie.c -o unepie
fa$ ./unepie
Y'a une pie dans le poirier,
J'entends la pie qui chante...
fa$

Pas de quoi s'émouvoir, je suis bien d'accord. Cependant..

Des formats différents!

Ubuntu 16.4
fa$ file -b ./unepie
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f528ec2c46a51d998c04d42b86d6d1a50331d378, not stripped

fa$
Debian 9.1
fa$ file -b ./unepie
ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=587bb830a101539c950ea7999b43442ebf7bd037, not stripped

fa$

Rire sardonique emprunté à DjiBee.

Sous Ubuntu 16.04, le binaire exécutable est conformément à nos attentes un binaire exécutable. Mais sous Debian 9.1 (et je crois Ubuntu 17), le binaire exécutable n'est pas, n'est plus un binaire exécutable. Et pourtant il s'exécute!

readelf à la rescousse ?

Ubuntu 16.4
fa$ readelf -l unepie

Elf file type is EXEC (Executable file)
Entry point 0x400430
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000744 0x0000000000000744  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000

fa$
Debian 9.1
fa$ readelf -l unepie

Elf file type is DYN (Shared object file)
Entry point 0x580
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000001f8 0x00000000000001f8  R E    0x8
  INTERP         0x0000000000000238 0x0000000000000238 0x0000000000000238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000008f4 0x00000000000008f4  R E    0x200000
  LOAD           0x0000000000000dd8 0x0000000000200dd8 0x0000000000200dd8
                 0x0000000000000258 0x0000000000000260  RW     0x200000


fa$

Sous Ubuntu, le binaire exécutable a les caractéristiques habituelles.  Sous Debian 9.1, le fichier ELF généré est différent: son type est DYN au lieu de EXEC mais il a un point d'entrée, et l'adresse virtuelle du texte est à 0 en lieu et place des 0x...0400000 proposés par Ubuntu.

Pourrait-on trouver d'autres exemples de fichiers de type "Shared object file" ?

Les librairies


Si on regarde la librairie C sous Debian 9.1:

Debian 9.1
fa$ readelf -l /lib/x86_64-linux-gnu/libc-2.24.so

Elf file type is DYN (Shared object file)
Entry point 0x203d0
There are 10 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000230 0x0000000000000230  R E    0x8
  INTERP         0x000000000016aee0 0x000000000016aee0 0x000000000016aee0
                 0x000000000000001c 0x000000000000001c  R      0x10
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000001948e8 0x00000000001948e8  R E    0x200000
  LOAD           0x00000000001957c8 0x00000000003957c8 0x00000000003957c8
                 0x0000000000004f38 0x00000000000091d8  RW     0x200000
 
fa$

C'est très similaire à notre binaire "unepie" sur Debian 9.1.
Notre binaire serait-il une librairie ?

Pas tout à fait.

On nous cache tout, on nous dit rien...

(Jacques Dutronc)

En fait, Debian 9.1 génère automatiquement les binaires exécutables sous forme
"PIE". Ça consiste à générer le texte et les données du binaire comme on le fait pour des librairies dynamiques partagées. Pour les librairies, on utilise l'option -fPIC. Ici, on utilise l'option -fPIE (pour le compilateur) et -pie (pour l'éditeur de liens).

Oui, mais ! Je n'ai rien demandé à gcc.

En fait, gcc ne demande rien, mais est pré-configuré pour compiler avec un certain nombres d'options. Ce que l'on peut observer avec l'option -v de gcc, comme ci-dessous:

Debian 9.1
fa$ gcc -v unepie.c -o unepie
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/6/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 6.3.0-18' ... --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ ...

 --enable-default-pie .... --target=x86_64-linux-gnu
Thread model: posix
gcc version 6.3.0 20170516 (Debian 6.3.0-18)
COLLECT_GCC_OPTIONS='-v' '-o' 'unepie' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/6/cc1 -quiet -v -imultiarch x86_64-linux-gnu unepie.c -quiet -dumpbase unepie.c -mtune=generic -march=x86-64 -auxbase unepie -version -o /tmp/ccQvdgZ6.s
fa$

Le résultat ci-dessus a été considérablement expurgé.
Si on effectue la même commande dans le monde Ubuntu 16.4, on s'aperçoit que l'option PIE n'est pas configurée par défaut.

Si on veut se débarrasser de la chose, il faut le dire au compilateur :


Debian 9.1
fa$ gcc unepie.c -o unepie -fno-PIE -no-pie
fa$ file -b unepie
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4b5d9a8c53c61f07d857612460a2cd2d7742a82d, not stripped

fa$

Pourquoi ?

La raison principale pour générer des binaires en mode PIE, est de pouvoir étendre la protection fournie par le mécanisme d'ASLR aux régions de code et de données. Sans le mode PIE, la protection est limitée aux régions de pile, de librairies partagées et aux régions créées par mmap.

Une curiosité pour finir


Debian 9.1 ou Ubuntu 16.4
fa$  /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu10) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
    crypt add-on version 2.1 by Michael Glad and others
    GNU Libidn by Simon Josefsson
    Native POSIX Threads Library by Ulrich Drepper et al
    BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
.
fa$

La librairie C est exécutable! (Merci à Xavier R.)
Elle donne la version!

Ce n'est pas vrai de toutes les bibliothèques partagées.  Seules quelques unes ont des droits rwx permettant une exécution.

Pour compléter

Et sinon

mardi 6 janvier 2015

Arithmétique des pointeurs...et fonctions...

Dans mon éternelle quête de l'inépuisable Graal, je viens de découvrir (naïf que je suis) que l'arithmétique des pointeurs révèle des surprises. Je passe outre le fait que les débordements de tableaux via les pointeurs sont des comportements plus étranges qu'il n'y paraît. Le standard indique à propos de la sémantique de l'arithmétique:
Section 6.5.6/8 Additive operators. When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated. 
Oui j'ai bien lu (pas vous ?): If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. Ce qui signifie en clair que l'on peut pointer à l'adresse qui suit immédiatement le dernier élément d'un tableau et que l'adresse stockée dans le pointeur est quelque chose une véritable adresse correctement calculée quand bien même on ne doit pas déréférencer le pointeur (c'est le end des itérateurs C++); mais pointer plus loin est un comportement non défini. Autrement dit, un programme faisant une telle opération ne peut être considéré comme juste, ce n'est pas un programme C! Attention c'est non-défini pas non-spécifié, par conséquent le compilateur/le runtime sont libres de faire ce qu'il veulent (y compris réinstaller Windows par-dessus votre cher Linux rien que pour vous punir).

Bon mais ma question originelle était le problème de l'arithmétique sur les pointeurs de fonction! Soit l'exemple suivant :
void f() {
  // super code génial
}
...
void (*pf)();
pf = f;
pf();
// Ok je vois bien
pf++;
pf(); // Argh!
...
Dans ce cas, il est évident que le second appel serait bien étrange, mais prenons l'exemple suivant:
void f() {
  // mega code trop bien
}
void g() {
  // hyper code trop cool
}
void magic(int i) {
  (f+(g-f)*i)();
}
L'idée étant que si on appelle magic(0) c'est f qui s'exécute et si on appelle magic(1) c'est g. Une compilation ordinaire ne dit rien et le code fonctionne. Si l'on active le standard c99, rien non plus. Par contre le mode pédant :
$ gcc --std=c99 -pedantic -c t.c
t.c:10:8: warning: arithmetic on pointers to the function type 'void ()' is a
      GNU extension [-Wpointer-arith]
  (f+(g-f)*i)();
      ~^~
t.c:10:5: warning: arithmetic on a pointer to the function type 'void ()' is a
      GNU extension [-Wpointer-arith]
  (f+(g-f)*i)();
Argh, l'arithmétique sur les pointeurs de fonctions est donc encore plus limitée ? Voyons ce qu'en dit le standard alors :
6.5.6/3 Additive operators. For subtraction, one of the following shall hold:
—  both operands have arithmetic type;
—  both operands are pointers to qualified or unqualified versions of compatible complete object types; or
—  the left operand is a pointer to a complete object type and the right operand has integer type.
What is object type ??? Appelons le standard de Cognacq-Jay:
6.2.5/1 Types. The meaning of a value stored in an object or returned by a function is determined by the type of the expression used to access it. (An identifier declared to be an object is the simplest such expression; the type is specified in the declaration of the identifier.) Types are partitioned into object types (types that describe objects) and function types (types that describe functions). At various points within a translation unit an object type may be incomplete (lacking sufficient information to determine the size of objects of that type) or complete (having sufficient information).
Bon ben ok, ben les pointers to functions types n'ont donc pas d'arithmétique! Ben voilà... Je suis déçu, encore une fois j'ai la preuve que je ne possède pas le Craal.

DjiBee

samedi 20 décembre 2014

Impact inattendu(?) sur les performances d'un programme. Vaz'y PAPI.

  • "Faire une mesure est relativement aisé. C'est quand on a obtenu la mesure que les emm... commencent." (Moi).
  • "Et j'aimerais bien savoir quand ils finissent!" (Re-moi) 
  • "Sans compter tous les emm... annexes."  (Toujours moi)
  • "Il y a beaucoup de mérite
    Sans compter les emmerdements.
    " (Extrait de "Qu’y a-t-il…?" de Boris Vian)

Indiquons

Soit un simple morceau de code qui cherche à exprimer la puissance de calcul d'un CPU, genre nombre d'instructions / de boucles  ou ce que vous voulez par secondes. Par exemple quelque chose qui ressemblerait aux calculs faits dans la procédure do_loop  utilisée dans Linux : CPU, core, HyperThread, mesures de temps d'exécution. De manière concrète, dans cet article, on a exécuté 4000 boucles (de 100000) en 10696 millisecondes, soit un indicateur de 4000/10,696 = 373,97 FauxPlat/sec(*) pour le processeur en question.

(*) Le FauxPlat/sec est une unité de puissance de CPU créée pour les besoins de ce billet. Le FauxPlat/sec est invariable : 1 FauxPlat/sec 2 FauxPlat/sec. Il n'y a pas d’abréviation officielle pour le FauxPlat/sec. FP/sec est prohibé en tant qu’abréviation de FauxPlat/sec. 

Blindons

Une fois écrit ce code, reste à s'assurer qu'il est exécuté dans des conditions relativement stables et protégées  des perturbations provoquées par l'exécution d'autres programmes. Le but : obtenir des valeurs  identiques ou raisonnablement proches pour des machines basées sur le même CPU. Ce qui devrait permettre (conditionnel dans un monde idéal) de comparer des machines...

Dans la mesure où la durée du calcul est relativement courte (~11 sec dans l'article cité), on peut se permettre d'exécuter ce calcul en utilisant une classe d'ordonnancement temps-réel (SCHED_FIFO) avec une priorité maximum. Pour limiter encore davantage les perturbations, l'utilisation de l'affinité processeur (sched_setaffinity) évitera de compter des coûts de migration CPU. Et comme les processeurs modernes sont de grands taquins et qu'ils ont la capacité de faire varier leur fréquence, on va aussi fixer la fréquence du processeur au maximum. (Euh, là, les bidouilles sont à découvrir vous-mêmes, à moins qu'elles ne soient livrées dans un billet ultérieur).

Ratons (pas laveurs)

Hmmm. Comme dit précédemment, la loi de Murphy indique que forcément non... Dans les faits, si vous pouvez (faire) réaliser ce type de calcul sur des centaines de machines, les variations relevées sur des machines de CPU identiques vont être surprenantes... de 1 à 3. Gloups. Pas systématique, la grosse majorité des résultats va varier dans une fourchette de disons 0 à 30%. Mais la masse de mesures joue contre vous et vous avez donc un nombre de mesures pour lesquellesvous ne savez pas justifier les variations.  Il peut arriver, expérience vécue*, qu'il faille changer la carte mère pour obtenir des résultats homogènes avec des CPUs identiques..

* euh? y'a des expériences pas vécues?

Piqué au vif soyons

Vous reprenez donc votre programme, et sur vos (quelques, car vous, vous n'avez pas accès à des centaines) machines, tentez de perturber ce calcul.
  • Essai n°1: Ajout d'une charge CPU importante sur le même processeur. Résultat : variation de 10% du nombre de FauxPlat/sec.
Regardez bien, ma sœur ;
Est-ce assez ? dites-moi ; n'y suis-je point encore? - Nenni. 
  • Essai n°2 : Ajout d'une charge CPU importante  faisant de nombreux accès mémoire pour perturber le cache L1. Résultat : variation de 20 % du nombre de FauxPlat/sec.
- M'y voici donc ?
- Point du tout.
  • Essai n° 3 : Ajout d'une charge CPU importante s'exécutant au même niveau de priorité temps-réel que le calcul de l'indicateur. Le calcul étant fait sur la mesure du temps CPU et non du temps mural... cela ne change rien, par ailleurs, il est relativement facile de prouver que cette situation ne semble pas être rencontrée sur vos machines de production...
M'y voilà ?
- Vous n'en approchez point.
Merci Mr Jean de La Fontaine.
  • Essai n°4 : Certes vous avez fixé la fréquence du processeur au maximum. Mais qui vous assure que cette fréquence n'est pas modifiée par un autre programme pendant vos exercices?  D'accord. Quand on fait varier la fréquence du simple au double, le nombre de FauxPlat/sec varie aussi du simple au double... Mmm Il y a là encore peu de chance que ce soit ce qui se passe sur vos machines de production...

10 wagons euh, divaguons

Fatigué, harassé, vous en êtes là et même un peu las de vos vaines et ridicules tentatives. Quand vous découvrez au hasard d'une errance webique la publication suivante (dont la lecture vous est vivement recommandée) : 

Producing Wrong Data Without Doing Anything Obviously Wrong!
Todd Mytkowicz, Amer Diwan, Matthias Hauswirth, Peter F. Sweeney
ASLPOS09 

où il est exposé et démontré que des mesures de performances peuvent être sérieusement altérées par des éléments n'ayant rien à voir ni avec la choucroute ni avec le yaourt. Il est dit qu'un programme aussi simple que ceci :

static int i = 0, j = 0, k = 0;
int main() {
    int g = 0, inc = 1;
    for (; g < 65536; g++) {

        i += inc;
        j += inc;
        k += inc;
    }
    return 0;
}

peut voir ses temps d'exécution  sérieusement modifiés selon... la taille des variables d'environnement du programme. Le graphique suivant extrait de l'article montre une variation du nombre de cycles nécessaires pour la boucle en fonction de la taille des variables d'environnement passées au programme. On voit très nettement, que dans un certain nombre de cas le nombre de cycles nécessaire est de 30% supérieur au cas "normal". Et dans un cas extrême il est de 300% supérieur au cas normal...

L'article décrit aussi des effets de bord extrêmement hilarants (de la Baltique) de l'ordre dans lequel les .o sont passés fournis lors de l'édition de liens sur les performances.

Reproduisons

L'expérience (la vôtre, donc la mienne en l’occurrence. Vous me suivez? Expérience donc tout aussi vécue que la précédente, même si ici, on ne se réfère pas à une expérience précise mais à l'Expérience que l'on pourrait définir comme le cumul de toutes les expériences vécues.)  L'expérience, donc, vous a rendu méfiant. Et vous voilà donc parti sur la piste de la répétition d'une expérience. Eh, oh. C'est un article scientifique... Qu'est-ce qui pourrait.. .

Les mesures ont été faites à l'aide de la bibliothèque PAPI (Performance Application Programming Interface) pour calculer le nombre de cycles utilisés pour la boucle...
  • téléchargement,
  • ./configure
  • make 
  • make install
Allons-y.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "papi.h"

#define NUM_EVENTS 1
long long values[NUM_EVENTS];
unsigned int events[NUM_EVENTS]={PAPI_TOT_CYC};
 

static int i = 0, j = 0, k = 0;

int main() {
    int g = 0, inc = 1;

    int retval;

    if((retval = PAPI_start_counters((int*)events, NUM_EVENTS)) 
        < PAPI_OK) {
         printf("argh %s\n", PAPI_strerror(retval));
         exit(1);
    }

    for (; g < 65536; g++) {

        i += inc;
        j += inc;
        k += inc;
    }

  
    if((retval = PAPI_stop_counters(values, NUM_EVENTS))
        < PAPI_OK) {
        printf("argh %s\n", PAPI_strerror(retval));
        exit(1);
    }

    printf("%lld\n", values[0]);

    PAPI_shutdown();

    return 0;
}

Vous (re)prenez des précautions élémentaires :
  • sur votre machine avec 24 processeurs, qui ne font pas grand chose, vous choisissez un processeur reculé au fin fond d'un core de la deuxième socket. Et vous jouez de l'affinité (taskset) pour être sûr que vous ne laisserez pas ce programme s'exécuter ailleurs.
  • Vous utilisez  aussi chrt pour vous donner une priorité bien méritée.
  • Si vous êtes vraiment parano, vous y allez même d'un coup de numactl, pour être certain que la mémoire va être allouée pas trop loin de votre processeur. (Note pour plus tard : faudra regarder ça de plus près ces trucs de NUMA.)
Et vous vous lancez dans un script qui
  • élimine toutes les variables d'environnement,
  • lance  100 fois votre programme avec comme seule variable d'environnement
  • A=
  • puis avec A=a
  • puis avec A=aa
  • etc jusqu'à 8192 ajouts.
Et vous calculez la moyenne des 100 exécutions faites pour chaque valeur de la variable d'environnement. Jusqu'à...

Un café sirotons

Bon, il faut plus d'un café... Chaque essai étant réalisé 100 fois pour chacune des 8192 tailles d'environnement, le résultat est disponible environ 1H45 après le lancement du script.


Déception! 

Le comportement décrit dans l'article  ne semble pas se reproduire. Certes, quelques mesures gigotent autour des 500 000 cycles, mais la moyenne étant à 490 000 cycles... nous sommes loin des 30 % de variations constatés dans l'article. Et pas aberration en vue à 300 %.

Persévérons

Oui, enfin... Au chat, notre langue donnons! Un échange avec un des auteurs de l'article conduit sur la piste de ASLR! Oui "Adresse Space Layout Randomization" décrit par DjiBee. La tentative précédente a été effectuée sur un système dans lequel ce mécanisme était activé.

Désactivons ASLR.


fa# echo 0 >/proc/sys/kernel/randomize_va_space
fa#

Et ré-essayons. Un autre café, et 1H45 plus tard:


C'est encore très différent du comportement observé dans l'article initial. Mais malgré tout, vous observez ici des variations entre 14% et 30 % par rapport à la moyenne. 

Zoomons

Si vous zoomez sur le premier pic, vous obtenez :


Et sur le deuxième pic :


Les 2 pics se ressemblent : 16 points chacun. Le delta entre les 2 n'est pas exactement  de 4096...Mais de 4093... Un peu décevant... Mais, tolérant soyons.

L'observation semble être répétable. Si vous avez quelques paquets de 1H45 à perdre, investir, gaspiller, il semble bien que vous obteniez sensiblement les mêmes résultats.

Ailleurs voir allons

Les essais précédents ont été faits sur une machine à base de processeurs Xeon E5-2420 en utilisant un compilateur GCC 4.4.6.
Donc, vous trouvez une autre machine, merci YuRug, avec 40 cores, donc 80 CPU. Et 1H45 plus tard, vous obtenez une variation entre le point minimum et maximum d'environ 2%...
"Rien ne t'affole." (Téléphone).

Vous vous apprêtez à relancer le test en désactivant l'ASLR... Vous pouvez vous avez le droit.

Capitulons

Oui, mais non... le système vous injurie. En disant que echo 0 dans randomize_va_space, là ça va pas le faire. uh ?Un coup de Google Est Ton Ami, et rien. Abandonné en rase campagne. Bon. Faudrait plonger  dans les sources du noyau.

Mais. C'est Décembre sérieusement avancé, comme un camembert. Faut aller remplir la hotte de PAPI (tiens?) Noël. Sans compter que vous avez un tas de 68 copies qui vous attend...

Alors... 

Concluons

Ah, ben. Pas trop avancé, là. Faudrait vraiment refaire cette expérience sur d'autres processeurs. Les impacts entre environnements et performance semblent bien possible. Si dans les lecteurs de ce billet il y a des amateurs qui veulent partager leurs résultats, la porte est grande ouverte! 

Est-ce que ça explique les variations de FauxPlat/sec  recherchées initialement ? Murphy traînant toujours dans les parages, j'ai du mal à y croire... 
  
"Il y a beaucoup de mérite
Sans compter qu’il y a des emmerdements."
(Boris Vian)

 

dimanche 9 novembre 2014

Codage et paresse, un exemple d'économie d'effort contre-productive.

Note liminaire

Aïe ! Je me suis fait mordre par un bogue, qui pour une fois n'était pas de mon fait. Pas le genre bogue enragé, mais qui (in)visible comme le nez au milieu de la figure ne s'est pas fait remarquer immédiatement.

Je dévisage rarement avec insistance l'appendice nasal de mes interlocuteurs... 
Mais, d'ailleurs, peut-on dévisager un nez ? 

Mise en scène

Bref. Je résume la situation. Une séquence de code (la création d'un processus) tout ce qu'il y a de plus banal dont on cherche à mesurer la durée. Un appel à gettimeofday au début, un autre à la fin. Une petite différence entre temps relevé à la fin et temps relevé au début, et hop.

Oui mais le temps résultant est négatif (très négatif!).  Ah, vous m'étonnez fort ! Auriez-vous inventé un programme à remonter le temps ?

Jetons un coup d’œil !

#include  <stdlib.h>
#include  <stdio.h>

int main (int argc, char **argv)
{
        struct timeval t0, t1;
        pid_t pid;

        gettimeofday(&t0, NULL);
        pid = fork();
        if (pid == 0) {
                return 0;
        }
        gettimeofday(&t1, NULL);
        wait();
        printf("temps: %ld µsec\n",
        ((t1.tv_sec - t0.tv_sec) *1000000L) +
         (t1.tv_usec - t0.tv_usec));
        return 0;
}


D'accord, codage minimaliste. Pas de test des codes retours, mais bon, soyons joueur !

Que donne la compilation ?

fa $ gcc lazzy_wb.c -o lazzy_wb
fa $

Super, ça compile sans erreur et sans warning. D'accord, quitte à être paresseux autant l'être aussi sur la commande de compilation.

Voyons l'exécution...

fa $ ./lazzy_wb
temps: -970715068 µsec
fa $

Ah, effectivement, nous voilà rajeunis d'environ 970 secondes.

Est-ce qu'on peut dire "le temps passe vite", quand il passe en marche arrière? 

Investiguons

Je vous la joue courte, sinon, Jérôme, va me dire "tout ça pour ça". :-)

Vous inspectez avec toute votre concentration le code, vérifiez le calcul de la différence. Rien, pas de loup caché ici. Un coup de strace et de ltrace pour vérifier si les appels systèmes se passent correctement. Toujours rien en vue, pas de frémissement dans ma synapse.

Allez, l'arme fatale ! Roulements de tambour: gdb.

Point d'arrêt après le deuxième "gettimeofday", vérification des valeurs de t0 et de t1.
Ben elles sont correctes.
Panne sèche...

Pfiouh les 970 secondes gagnées il y a peu sont en train de s'évaporer dans le présent / futur à vitesse grand V!

Et puis d'un coup d'un seul ! Le nez, là, au milieu de la figure ! Eh, oh! Y'a pas de nez!

Un petit coup de RTFM, man wait....

Ah, ben tiens, wait prend un paramètre, intempestivement  passé aux oubliettes dans notre code.

Essayons pour voir!


fa $ sed -i -e '/wait(/s/wait(/&NULL/' lazzy_wb.c
fa $ gcc lazzy_wb.c -o lazzy_wb
fa $ ./lazzy_wb
35 µsec
fa $

Pardon ? Passer "NULL" comme paramètre à wait suffit à corriger notre problème?

Analyse

wait, s'il reçoit un paramètre non NULL, le considère comme un pointeur sur entier et copie dans l'entier ainsi pointé la valeur de retour du processus fils décédé. Dans notre cas, 0. Ce que confirme gdb après le wait le champ t1.tv_sec est bien écrasé à 0. D'où une différence de temps négative.

Un coup d’œil à l'assembleur

Sans le paramètre

    movl      $0, 4(%esp)
    leal        40(%esp), %eax
    movl      %eax, (%esp)
    call        gettimeofday
    movl      %eax, 24(%esp)

    call        wait

Avec le paramètre

    movl       $0, 4(%esp)
    leal         40(%esp), %eax
    movl       %eax, (%esp)
    call         gettimeofday
    movl       %eax, 24(%esp)
    movl       $0, (%esp)
    call         wait

Quand on invoque wait avec NULL, on empile effectivement une valeur nulle sur la pile comme paramètre de wait. Sans paramètre, on ne fait, rien... et wait qui croit qu'on a mis une valeur en pile, va utiliser ce qui se trouve en sommet de pile... et pas de chance, c'est le dernier paramètre poussé sur la pile par le compilateur... à savoir l'adresse de t1. Bingo!

Morale(s)

  •  Ne pas passer de paramètre n'est pas équivalent à passer un paramètre NULL. Ma fainéantise naturelle me susurre que c'est bien dommage. 
  • Une compilation avec -Wall nous aurait indiqué que les prototypes de fork, wait et gettimeofday n'étaient pas définis.
  • Après ajout des directives d'inclusion idoines, nous aurions eu le message suivant  error: too few arguments to function ‘wait’

Bilan

2 à 3 minutes épargnées à ne pas effectuer les inclusions appropriées.
10 minutes perdues à 2 pour identifier et corriger.

Plus une heure pour écrire ce billet, et je ne compte pas le temps que vous venez de perdre à le lire.
Tout ça pour ça!
Désolé!

mercredi 29 octobre 2014

Vacances au sous-shell


Bon. J'ai raté la fin des vacances et la rentrée... Mais je suis juste à temps pour la rentrée des vacances de la Toussaint, non ? 

Jour de la rentrée dans la cour de récréation...

  • Octet Touffu : Alors, ces vacances?
  • Octet Foutu  : M'en parle pas, je me suis perdu dans les sous-shell.

Égarement

Le temps des vacances. Flânerie, on écrit quelques lignes de shell-script... Non, je n'ai pas dit avoir codé du shell script pendant mes vacances... mais pendant le temps des vacances... des autres. Nuance.

Et hop, une ligne en appelle une autre, du coup voilà une fonction... qui doit faire une boucle et récupérer un résultat facile... Je simplifie et vous épargne les 1 500 lignes de script environnantes !
Oui, je sais, quand on atteint ce volume, on s'est trompé de langage. Python serait probablement plus adapté. Vous ne faites jamais d'erreur vous?

 Et une fonction shell, une.

function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         FS_ARRAY[$CNT]=$FS
         CNT=$((CNT + 1))
    done

Facile. On récupère le premier champ de champ de chaque ligne utile imprimée par df, et on le stocke dans un tableau. Du coup, après on espère pouvoir utiliser le tableau!

Essai

fa# get_fs
fa# echo ${FS_ARRAY[0]}


fa# Euh, y'a rien là au dessus?

Pardon?  Eh? Où est la valeur de mon tableau?
Oui, je vous vois, tordus, pliés, rigolards.

N'empêche, persévérons

On met donc des traces dans la fonction et on réessaye ! Pour plus de sûreté, on trace l'entrée du tableau après son initialisation et le compteur après son incrémentation.

fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         FS_ARRAY[$CNT]=$FS
         echo ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
/dev/sda1
1
udev
2
tmpfs
3

fa# echo ${FS_ARRAY[0]}

fa# Euh, y'a toujours rien là au dessus?

Souvenirs, souvenirs

Et là, se profile l'idée que peut-être  la fonction procéderait au moyen d'un sous-shell, qui initialiserait sa copie de FS_ARRAY... mais que cette copie serait somme toute perdue quand on quitte le sous-shell. Et donc, il faudrait trouver un autre moyen pour remplir ce tableau. Ah, oui le shell, les sous-shells.

Mais, vexé, on voudrait quand même savoir si c'est bien  un sous-shell la cause de notre perplexité, sans parler de la déception. 

Et donc on vérifie!

Imprimons donc le PID de nos shells et sous-shells avec la variable $$ .

fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         FS_ARRAY[$CNT]=$FS
         echo shell $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
shell 2130 sets /dev/sda1
shell 2130 sets udev
shell 2130 sets tmpfs

fa# echo $$
2130
fa# Euh, Le shell et le sous-shell auraient le même pid? Ou il n'y a pas de sous-shell? Qu'est-ce que cette embrouille?

Je ne crois même pas ce que je vois !

Comme on subodore une entourloupe et qu'on connaît un peu son système, si on allait chercher nous même le pid du processus? Il faut quand même une bonne dose de paranoïa pour ne plus faire confiance au shell pour quelque chose d'aussi simple que echo $$ !
 
fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         read -r MYPID GARBAGE
        FS_ARRAY[$CNT]=$FS
         echo shell $MYPID $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
shell 5707 2130 sets /dev/sda1
shell 5707 2130 sets udev
shell 5707 2130 sets tmpfs

fa# read -r MYPID GARBAGE
fa# echo $MYPID $$
2130 2130
fa# Ah! Ah! Aurais-je trouvé un bug grossièrement trivial? Pourquoi $$ donne-t-il 2130 dans les 2 cas, alors que /proc donne 2130 pour l'un et 5707 pour l'autre?
       
Donc, le pid du sous-shell diffère de $$ ? Des certitudes s'effondrent. Retour aux sources: RTFM.

RTFM ! Et le manuel nous dit:

$   Expands to the process ID of the shell. In a () subshell, it expands to the process ID of the current shell, not the subshell.

En clair, le manuel nous dit, apprend à lire. En fait, je ne sais pas si j'ai jamais relu cette partie depuis la page manuel d'un bon vieux shell Bourne d'un Unix V7.

 Vérification! Un coup d’œil dans un bon vieux manuel V7 trouvé là http://plan9.bell-labs.com/7thEdMan/v7vol1.pdf
$ The process number of this shell.

Ah flûte, le monde évolue ! On a modifié la sémantique de de $$ sans m'avertir ! Serait-il nécessaire de se tenir au courant  ?

Simplifions avec BASHPID et BASH_SUBSHELL

fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
        FS_ARRAY[$CNT]=$FS
         echo shell $BASHPID $BASH_SUBSHELL $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
shell 5715 1 2130 sets /dev/sda1
shell 5715 1 2130 sets udev
shell 5715 1 2130 sets tmpfs

fa# echo $BASHPID $BASH_SUBSHELL $$
2130 0 2130
fa# 

 Voilà! $BASHPID est la variable qui indique réellement le pid du processus shell courant. et $BASH_SUBSHELL est une indication d'un niveau de sous-shell. A utiliser la prochaine fois qu'un cas douteux se présentera.

Tatillons

Je vous connais, lecteurs tatillons ! D'accord, il sait maintenant déterminer si le code est exécuté dans un sous-shell ou non, mais il ne peut toujours pas récupérer le tableau initialisé par la fonction dans le shell principal. Voilà un moyen comme un autre.


fa# function get_fs()
{
    for FS in $(df  | sed -e 1d | awk '{print $1}')
    do
        FS_ARRAY[$CNT]=$FS
         echo shell $BASHPID $BASH_SUBSHELL $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))

    done


fa# get_fs
shell 6432 0 6432 sets /dev/sda1
shell 6432 0 6432 sets udev
shell 6432 0 6432 sets tmpfs

fa# echo$CNT ${FS_ARRAY[0]}
7 /dev/sda1


fa# Ah, enfin!

Bref

Le shell (et spécialement les sous-shells) contrairement au vélo, ça s'oublie.
Il faut toujours lire les manuels des nouvelles versions.