Skip to content

Linux Attitude

Le libre est un état d'esprit

Archive

Archives pour novembre, 2008

Liens

nov 28

J'ai un certain nombre de liens en vrac sur des choses intéressantes, mais pour lesquelles je ne prendrai pas le temps d'écrire. Je vous passe ici des liens vers des sites qui en parlent très bien.

Je pense que vous aurez assez de lecture avec ceci, mais je reviendrai ...

Niveau : Star Star Empty Empty Empty
Résumé : tail -F ; ls | wc ; xmodmap ; bell-style ; /proc/sys/kernel/core_pattern

Lire un fichier au fur et a mesure qu'il est complété (par exemple un fichier de log) :

# -F bon réflexe pour les fichier qui peuvent être rotatés
$ tail -F fichier

Compter les entrées d'un répertoire sans se planter avec les noms spéciaux (espace ...) Perdre son temps

$ find . -maxdepth 1 -mindepth 1 -printf a | wc -c

Transformer votre souris en souris pour gaucher :

$ xmodmap -e "pointer = 3 2 1"

Supprimer les bips en bash :

$ set bell-style none

Transformer les bips en flash écran en bash :

$ set bell-style visible

Retour au comportement par défaut :

$ set bell-style audible

Faire apparaitre tous les core dump dans /tmp et non dans le cwd du processus :

# on en profite pour lui donner un nom unique <binaire>-<date>-<pid>.core
$ echo /tmp/%e-%t-%p.core > /proc/sys/kernel/core_pattern

Pour rendre cela persistant au reboot, écrire la ligne suivante dans /etc/sysctl.conf :

kernel.core_pattern=/tmp/%e-%t-%p.core

Niveau : Star Star Star Empty Empty
Résumé : chmod +x test.c

Après une technique utilisant du perl, voici une nouvelle façon d'utiliser du C aussi simplement qu'un script.

Cette fois nous évitons l'usage de perl, on se passe donc de ses avantages en terme de parsing. Par contre, on utilise un en-tête court et le code fonctionne avec n'importe quel code valide en C. Comme d'habitude, on fait propre et on renvoie le bon code de retour.

#!/bin/sh
tail -n +4 $0 | gcc -Wall -o /tmp/cscript.$$ -x c - && /tmp/cscript.$$ $*
ret=$? ; rm -f /tmp/cscript.$$ ; exit $ret
//
// Code C
//
#include <stdio.h>
int main(int argc, char** argv)
{
        printf( "Appel de %s avec %d arguments\n", argv[0], argc-1 );
        return 0;
}

Et maintenant on teste pour prouver que ça marche :

chmod +x test.c
./test.c 1 2 3 && echo "OK" | | echo "KO"

Niveau : Star Empty Empty Empty Empty
Résumé : la vie avec unix

Unix, ce n'est pas seulement un système, ce n'est pas seulement une marque, c'est aussi une façon de voir les choses, une façon de concevoir des programmes qui feront ce dont vous avez vraiment besoin. Nous allons parler des principes fondamentaux d'unix.

Tout est fichier

On commence à le savoir, les périphériques sont des fichiers qui se trouvent dans /dev, les processus sont des fichiers qui se trouvent dans /proc, les paramètres et données du noyau sont des fichiers qui se trouvent dans /sys et accessoirement /proc, les exécutables sont des fichiers normaux. Seule la carte réseau n'est pas un fichier, pour des raisons de performances paraît-il. Mais les connexions sont tout de même vues à travers des descripteurs de fichier dans les processus.

Les données sont du texte

Dans la philosophie unix, toutes les données doivent être stockées et transmises sous forme de texte. Cela peut coûter légèrement plus de place qu'un format binaire. Mais on y gagne beaucoup.

  • Un fichier texte peut être lu par les autres outils unix, et donc respecte de principe faire une chose et le faire bien
  • Un fichier texte peut être lu par un être humain et donc respecte le principe kiss
  • Un fichier texte permet une interopérabilité avec d'autres systèmes (pas de problèmes d'indiens en iso par exemple)
  • Un fichier texte facilite le débugage

On retrouve donc le texte partout :

  • Dans les scripts
  • Les fichiers de configuration
  • Des formats de données d'application
  • Dans les protocoles réseau de haut niveau
  • La sortie d'informations se vos programmes

On ne le retrouve pas dans les cas où l'espace ou le temps de parsing sont primordiaux, par exemple dans les formats d'image ou de son.

Et la tendance va en s'accentuant puisqu'on utilise de plus en plus un format texte structuré un peu partout et à toutes les sauces : le xml.

KISS

Keep it simple, stupid !

Une petite question se pose sur le sens de stupid. Est-ce un deuxième adjectif présent pour renforcer le simple ou un qualificatif destiné au lecteur ? Je vote pour la première solution, mais ce n'est que mon avis.

Il s'agit ici de disposer d'outils très simples. Plus c'est simple mieux c'est. Ce n'est pas que l'utilisateur soit stupide, c'est qu'il n'a aucune raison d'aller perdre du temps à comprendre le fonctionnement de quelque chose qui est fondamentalement simple. Qui dit outils simples, dit faciles à manipuler, faciles à réutiliser.

Mais on ne parle pas seulement d'usage ici. Il s'agit aussi de garder le code simple, stupide. Cela rejoint un propos sur les commentaires dans le développement. Si votre code est bien écrit, bien architecturé, simple, alors il sera aussi simple à lire, à maintenir et à débuger, pas besoin de milliers de lignes de commentaires.

Faire une chose et le faire bien

Unix a inventé le pipe '|', une méthode simpliste pour faire communiquer 2 programmes entre eux. Cette communication est unidirectionnelle et non formatée, seules des données brutes y passent. Et pourtant, à lui seul le pipe a fait beaucoup plus pour unix que n'importe qui.

Grâce au pipe et au fait que la plupart des programmes communiquent avec un format texte, il est possible de faire des programmes qui ne soient que des filtres, qui ne fassent qu'une chose sur les données qu'on leur donne, et qui le fassent bien. Il est possible de faire des choses qui n'étaient pas prévues mais qui sont rendues possibles par l'apparition d'un nouveau filtre.

Bien sûr après le pipe, on a inventé la socket qui communique dans les 2 sens, ainsi que bien d'autres moyens de communiquer d'un processus a l'autre. Mais ce ne sont que des évolutions servant à pousser toujours un peu plus loin le principe : faire une chose et le faire bien. Quel que soit ce que vous développez, arrangez-vous pour que votre logiciel sache communiquer avec les autres logiciels unix. Ainsi vous garantirez une utilisation bien plus large à votre produit que celle que vous aviez dans la tête.

Puisque les autre outils sont déjà des outils simples qui font une chose et bien, vous pouvez aussi les réutiliser et vous appuyer sur les épaules d'un géant.

Quelques ressources ici et .

Niveau : Star Star Empty Empty Empty
Résumé : table des partitions

Un disque dur est en général divisé en partitions. Je dis en général, car ce n'est pas obligatoire, une disquette ne l'est quasiment jamais.

Sur un PC, la table est en général au format DOS (nommé aussi MBR), je dis en général car ce n'est pas le seul format possible, même si c'est le plus courant.

Nous allons donc se restreindre au cas d'un disque dur partitionné au format DOS.

Premier secteur

Les informations de partitionnement se situent sur le premier secteur (512 octets) d'un disque. Mais il n'y a pas que ça. On y trouve des nombres magiques, et du code pour démarre la machine. La table des partitions n'est constituée que de 64 octets (de 446 à 510). On appelle ce secteur le master boot record (MBR).

mbr.png

Pour lire les données de votre MBR :

$ dd if=/dev/hda of=/tmp/mbr bs=512 count=1
$ xxd /tmp/mbr

La table contient 4 entrées de 16 octets, qui forment les fameuses 4 partitions primaires, limite originelle du nombre de partitions.

mbrtable.png

Chaque entrée décrit le début, la longueur et quelques attributs d'une partition. Historiquement, on utilisait la notion de cylindre, tête et secteur mais cette notation est obsolète, inefficace en terme de place et ne correspond plus à la réalité. On utilise donc un comptage sur 4 octets du nombre de secteurs (512 octets) sur le disque, qu'on appelle LBA. Comptez, cela fait 2To maximum, vous aurez peut-être un problème de partitionnement avec votre prochain disque, il faudra passer à un format un peu plus moderne ... patientez pour un prochain article.

Maintenant, je suis sûr que vous voulez savoir à quoi ressemble votre table :

# Contenu quasiment brut
$ sfdisk -d /dev/hda
# le même un peu traduit
$ sfdisk -l -uS /dev/hda

Et là vous vous dites : mais il y a plus que 4 partitions !

Les partitions logiques et étendues

Hé oui, des gens ont voulu pouvoir avoir bien plus que 4 partitions, c'est comme ça qu'on a inventé les partitions logiques. Question vocabulaire, une partition étendue est une partition primaire qui va contenir d'autres partitions qu'on appelle les partitions logiques.

Donc si vous lisez bien votre table, il est probable qu'elle contienne un hda2 (étendue, non visible sous linux) englobant la totalité du disque à l'exception de la première partition. Les partitions hda5 et hda6 (logiques) sont alors dans cette dernière.

Le format de partition étendue est un peu consommateur (512 octets) puisqu'on réécrit un nouveau secteur pour chaque partition au même format que le MBR. On n'y met cette fois que la partition logique et la description de la prochaine partition (ce qui forme une liste chaînée). Cela veut dire qu'on peut avoir autant de partitions étendues qu'on veut (par défaut linux n'en peut gérer que 64).

Espace perdu

Comme dit un peu plus haut, historiquement on utilisait la notation C/H/S (cylinder, head, sector) pour décrire les positions des partitions. Cette notation impliquait qu'on mette la première partition dans la 2e piste puisque le MBR se trouvait dans la première. On a gardé cette convention même si elle n'a rien d'obligatoire. Ce qui laisse de l'espace libre dans la première piste.

Une piste est constituée des secteurs d'un cylindre sur une tête de lecture. Ce qui nous donne :

  • Taille Secteur = 512 (défaut sur quasiment tous les disques)
  • Taille Piste = 512 * nombre de secteurs par piste (S)
  • Taille Cylindre = 512 * nombre de secteurs par piste (S) * nombre de têtes (H)
  • Taille du disque = 512 * nombre de secteurs par piste (S) * nombre de têtes (H) * nombre de cylindre (C)

Elle a en général une taille de 512*63 soit 32256 octets inutilisés sur un disque (et autant pour chaque partition logique si vous avez bien suivi). C'est ici que certains outils anti-piratage mettaient leurs données à l'époque où windows existait encore. Maintenant un bootloader comme grub va profiter de cet espace pour s'y loger et éviter d'être dérangé par les systèmes.

Grub premier du nom va y mettre son stage1.5 et grub2 est capable d'y rentrer en entier.

Niveau : Star Star Empty Empty Empty
Résumé : optiong ; unfoo ; shopt -s dotglob ; GLOBIGNORE

Ah, le grand retour du vrac, ça faisait longtemps !

Reduire la taille d'un png (sans perte) :

# il semble en général meilleur que pngcrush
$ apt-get install optipng 
# modifie le fichier inline
$ optipng fichier.png 

Décompression de n'importe quel format

# script de décompression 
$ sudo wget -O /usr/local/bin/unfoo http://obsoleet.org/code/unfoo/unfoo-1.0.6.sh
$ chmod +x /usr/local/bin/unfoo
# on cherche à décompresser toto.truc, on se fout du format
$ unfoo toto.truc

Inclure les fichiers commençant par . dans * :

# option de bash
$ shopt -s dotglob
# et hop ils apparaissent
$ echo *

Inclure les fichiers commençant par . dans * (bis) :

$ export GLOBIGNORE="."
$ echo *

Obtenir la taille d'un périphérique de bloc (comme un disque) :

$ blockdev --getsize64 /dev/sda1

Raccourci pour connaître l'heure actuelle chez votre correspondant à l'autre bout de la planète :

$ alias pdate="TZ='Pacific/Noumea' date"

Niveau : Star Star Star Star Empty
Résumé : SSE

Aujourd'hui l'avenir (et un peu le passé aussi). Vous souvenez-vous des cray ? Ces machines surpuissantes qui avaient au moins la puissance de calcul d'une TI92. Comment faisaient-ils pour avoir une telle puissance ? Il faisaient des calculs identiques en parallèle et multipliaient ainsi par n le nombre d'opérations. C'est ce qu'on appelle une architecture vectorielle.

Intel dans sa grande bonté a commencé à mettre en place une telle architecture dans ses processeurs. Tout d'abord avec les instructions MMX. Ces instructions utilisent les registres servant au calcul en virgule flottante (quelques transistors de gagnés), lesquels font 64 bits. On peut ainsi exécuter des instructions identiques simultanément sur 4 fois 1 octets ou sur 2 fois 2 octets. Ce qui est bien, mais pas top.

Le SSE est une amélioration du concept. Tout d'abord on utilise de nouveaux registres (ce qui libère les calculs en virgule flottante) et on leur donne une taille de 128bits. Du coup on peut manipuler deux fois plus d'entiers et cela devient intéressant pour les flottants. On peut caser 4 flottants simple précision et 2 flottants double précision dans 128 bits. Le SSE s'arrête aux 4 flottants et le SSE2 ajoute quasiment toutes les autres combinaisons possibles (flottants et entiers).

Version normale

Comme nous sommes joueurs, nous allons jouer à améliorer notre code de calcul de Mandelbrot. Mais cette fois, nous allons le laisser tourner sous linux. Comme ne sommes plus dans notre monde, mais sur un vrai système, nous allons devoir faire quelques modifications. Nous allons continuer à écrire directement en mémoire, donc on garde nos fonctions d'écriture de pixels, mais cette fois dans une mémoire allouée avec malloc. Ajoutons le support de l'écriture d'un des format d'image les plus simples, le BMP, pour stocker notre image dans un fichier et pouvoir admirer le résultat. Pour cela on prend la première documentation venue.

/* Ecriture de l'image dans un fichier bitmap (on écrit l'en-tête kivabien puis les données) */
void writebitmap(char* filename, char*bitmap, int width, int height);

Et voilà, cela nous donne une première version du code quasiment identique à la précédente et donc non optimisée pour le SSE (disponible à la fin de l'article).

Version SSE

Commençons à travailler en découvrant les instructions du sse. Celles-ci sont rendues disponibles en C à travers les en-têtes qui vont bien. Ouf, cela nous évitera de coder directement en assembleur :

/* mm=mmx   xmm=sse  emm=sse2   pmm=sse3 (rien que de très logique ...) */
#include <emmintrin.h>

Il est possible de lire ces en-têtes pour savoir quelles sont les instructions et les types disponibles. C'est un peu long et assez peu instructif lorsqu'on ne connait pas la base du sse, mais ils peuvent servir de référence quand on sait ce qu'on cherche. Ils se trouvent dans /usr/lib/gcc/x86_64-linux-gnu/4.1.2/include/ (à adapter à votre cas). Les types et fonctions du sse sont codifiés.

Les opérations sont construites comme suit _mm_XXXX_YY :

  • _mm est un préfixe
  • XXXX est le nom de l'instruction en assembleur (à quelques variations près)
  • YY est le type des opérandes manipulés :
    • sd : double seul (1 double qui n'occupe pas tout le registre)
    • pd : double packé (2 doubles dans un registre)
    • ss : flottant seul (1 flottant qui n'occupe pas tout le registre)
    • ps : flottant packé (4 flottants dans un registre)
    • ...

Les types de données sont aussi définis selon un certain schéma :

  • __m128 : registre de 128 bits
  • __m128i : registre de 128 bits destiné au calcul entier
  • __m128d : registre de 128 bits destiné au calcul flottant
  • __v4sf : vecteur de 4 simple flotants (4 * 32 bits)
  • __v2di : vecteur de 2 entiers doubles (2 * 64 bits)
  • ...

Notez que d'un point de vue binaire, ils sont tous identiques et font 128bits.

Maintenant nous allons modifier le code pour qu'il calcule les points 4 par 4. On va donc changer nos 2 fonctions de calcul en :

/* conversion de coordonnées 4 points par 4 */
void convert_i2d4(int x, int y, __v4sf *ppx, __v4sf *ppy)
/* une itération du calcul pour 4 points */
__v4sf iteration4(__v4sf cx, __v4sf cy, __v4sf *ppx, __v4sf *ppy)

On utilise un des types précédents : __v4sf. C'est un vecteur de 128 bits contenant 4 flottants simple précision. Mais comme nous n'utilisons plus des types standards du C, nous devrons utiliser des fonctions à la place des opérateurs +, -, / et *, ce qui rend le code un peu plus lourd à lire et à écrire, par exemple :

        /* t = p^2 + C */
        tx = _mm_add_ps( _mm_sub_ps(_mm_mul_ps( *ppx, *ppx),  _mm_mul_ps(*ppy, *ppy)) , cx);
        ty = _mm_add_ps( _mm_mul_ps(two, _mm_mul_ps(*ppx, *ppy)), cy);

On y ajoute quelques constantes globales du fait que les constantes n'existent pas pour les type vectoriels :

__v4sf two = _mm_set_ps(2,2,2,2);

Et enfin, on doit adapter un peu notre algorithme pour qu'il ne s'arrête que lorsque les 4 points ont tous dépassés le seuil. Pour que la bonne couleur soit bien enregistrée séparément pour chaque point, on va faire la comparaison sur les 4 points grâce à _mm_cmple_ps qui renvoit 0 ou -1 sous forme de vecteur d'entier. Il nous suffit d'additionner les résultats pour récupérer la couleur de chaque point telle que nous l'avions définie plus tôt.

    /* calcul */
    radius = iteration4(cx, cy, &px, &py);
    /* on compare au rayon max, la valeur de retour est constituées de 4 entiers valant 0 ou -1 */
    jj = (__m128i)_mm_cmple_ps(radius, eight);
    /* et on additionne tous les -1 */
    ii = _mm_add_epi32(ii, jj);
    /* un int fait 32 bits sur les i386 et les x86-64 ... */
    ptr = (int*)&jj;
    /* il y a un moment où ce n'est plus la peine de calculer */
    if(ptr[0] == 0 && ptr[1] == 0 && ptr[2] == 0 && ptr[3] == 0)
        break;
 

Le code complet est disponible à la fin de l'article.

Résultat

Mais cela vaut-il le coup ? Oui, et nous allons le montrer. Tout d'abord sur un processeur 32 bits. Nous allons comparer plusieurs combinaisons. Avec sse, sans sse, avec différents niveaux d'optimisation, avec notre code spécifique, et sans.

exécution sur un i386 (en secondes)
Optimization - -O1 -O2 -O3
Gcc parameter - -msse2 - -msse2 - -msse2 - -msse2
normal code 6.20 6.40 3.4 3.5 2.95 2.65 2.60 2.35
SSE code - 4.75 - 0.91 - 0.55 - 0.50

Les résultats sont assez parlants. Que peut-on en déduire ? Que le sse ne semble pas très performant en l'absence d'optimisation (nosse.c et colonnes sans optimisation ou avec -O1).

Ensuite, que même sans utiliser le sse, gcc est lui-même capable de nous faire gagner beaucoup en performances avec ses optimisations. Mais gardez à l'esprit que cela est surtout valable parce qu'on utilise beaucoup de calculs en virgule flottantes et que nos fonctions prennent de gros arguments.

Enfin, que la modification de notre algorithme pour profiter du sse nous a permis de faire mieux que x4 sur du code déjà optimisé par gcc. En effet, gcc optimise ce qu'il peut, mais il lui est très difficile de changer un algorithme. Il est prévu que de nouvelles versions de gcc sachent vectoriser automatiquement des calculs, mais il faudra tout de même faire attention à la façon dont on code.

exécution sur un x86-64 (en secondes)
Optimization - -O1 -O2 -O3
Gcc parameter - -msse2 - -msse2 - -msse2 - -msse2
normal code 2.10 2.10 1.05 1.05 0.76 0.76 0.72 0.72
SSE code 2.10 2.10 0.30 0.30 0.30 0.30 0.30 0.30

Et enfin regardons le même code sur un processeur 64 bits. Tout d'abord on remarque que ma machine 64 bits est plus puissante que la précédente :-) Ensuite, on constate qu'il n'y a pas de différence entre l'option sse et sont absence. Et heureusement puisque tous les processeurs 64 bits ont le sse par défaut et le compilateur ne se gène pas pour l'ajouter lui-même. On remarque que comme pour le code 32 bits, il y a un facteur 3 entre l'option -O3 et l'absence d'optimisation pour le code normal.

Par contre, les différents niveaux d'optimisation au delà de -O1 n'influent plus sur la vitesse d'exécution. Je n'ai pas d'explications à cela.

Et enfin on constate que le sse a réduit la différence de puissance entre les 2 machines.

Pour ceux qui se demanderaient si on mesure les bonnes choses, un test écrivant tous les pixels en noir a été fait, et il ne prend pas plus de 0.03s sur ces plate-formes (toujours moins de 10%). Ce qui veut dire que nos comparaisons sont pertinentes (et qu'il n'est pas nécessaire d'optimiser cette partie).

Vous voulez tout savoir sur le SSE ? Arstechnica a publié un très bon article sur le sujet. Sinon, vous avez toujours la liste détaillée des instructions.

PS : Intel trouvant que la vectorisation est une bonne chose et voyant que tout cela marche bien a prévu d'ajouter les instructions AVX dans ses prochains processeurs. En gros, c'est la même chose que le SSE mais sur 256 bits cette fois.

PJ :