Introduction Comme vous le savez sans doute, un processeur ne connaît ni les nombres entiers ni les nombres réels, ni les chaînes de caractères. La seule chose que votre processeur sait traiter ce sont des bits (binary digit) bref des niveaux électriques. Comme en C, il existe en PHP des opérateurs qui permettent de manipuler directement ces bits contenus dans les variables avec lesquelles nous travaillons quotidiennement en PHP. Pourquoi, me direz vous utiliser ces opérateurs peu attrayants? Eh bien pour les performances. Ils permettent en effet, d'améliorer les performances de leurs scripts en limitant au maximum le nombre d'étapes nécessaires pour parvenir à des instructions compréhensibles par le processeur. Tous les opérateurs sur les bits correspondent à des instructions directement compréhensibles par votre processeur. Bref, ils permettent de mâcher le boulot de l'interpréteur, (pour les langages de scripts ex : PHP) ou des compilateurs pour les langages compilés (C/C+). Rappel de notions élémentaires : comme nous allons manipuler des bits il est important que vous compreniez la notions de poids des bits dans une variable. Le poids d'un bit c'est un peu l'indice de sa position dans le tableau de bit que constitue la variable. 1.2 Exemple En binaire, l'entier 3 s'écrit 00000011 et 2 s'écrit 00000010 $a=3; $b=2; $c=$a^$b; $a vaudra 3 soit en binaire 00000011 $b vaudra 2 soit en binaire 00000010 $c vaudra 1 soit en binaire 00000001 On s'aperçoit ici que si $a égale $b, $c vaudra 0. On peut donc trouver une écriture alternative à cette phrase : if($a!=$b) die("\\$a est différents de \\$b" En utilisant Xor on a : if($a^$b) die("\\$a est différents de \\$b" Si $a=18 (00010010) les bits à 1 sont les bits de poids 1 et 4. Poids 7 6 5 4 3 2 1 0 Bits de $a 0 0 0 1 0 0 1 0 $a se calcule donc de la façon suivante : $a=1*pow(2,4)+1*pow(2,1 ou $a=1*16+1*2; Présentation des divers opérateurs 2 AND (&) 2.1 Introduction Le résultat d'un & entre $a et $b est le suivant : chaque fois que 1 bit de $a, est à 1 en même temps que celui de $b, le résultat retourné voit son bit de même poids passer à 1. 2.2 Exemple En binaire, l'entier 3 s'écrit 00000011 et 2 s'écrit 00000010 1 XOR (^) 1.1 Introduction Le résultat d'un Xor entre $a et $b est le suivant : chaque fois que 1 bit de $a ne correspond pas au bit de $b de même poids, le résultat retourné voit son bit de même poids passé à 1. NB :On ne prendra en compte que les 8 bits de poids faible de l'entier qui est lui codé sur 32 bits (31 + un bit de signe) $a=3; $b=2; $c=$a&$b; $a vaudra 3 soit en binaire 00000011 $b vaudra 2 soit en binaire 00000010 $c vaudra 2 soit en binaire 00000010 08/16/02 1
3 OR ( ) 3.1 Introduction Le résultat d'un entre $a et $b est le suivant : chaque fois que 1 bit de $a ou de $b est à 1, le résultat retourné voit son bit de même poids passé à 1. 3.2 Exemple 5.2 Exemple $a=5; // 00000101 en binaire $b=$a<<1; $c=$b<<3; En binaire l'entier 5 s'écrit 00000101 et 3 s'écrit 00000011 Donc si on a: $a=5; $b=3; $c=$a $b; $a vaudra 5 soit en binaire 00000101 $b vaudra 3 soit en binaire 00000011 $c vaudra 7 soit en binaire 00000111 4 NOT (~) 4.1 Introduction $a vaudra 5 soit 00000101 en binaire $b vaudra 10 soit 00001010 en binaire $c vaudra 80 soit 01010000 en binaire Remarquez que chaque décalage correspond à une multiplication par 2 de la valeur. 6 Décalage à droite (>>) 6.1 Introduction Cet opérateur permet de décaler les bits d'une variable n fois vers la droite. Le résultat de l'opération correspond non pas aux bits qui sont sorti mais ceux qui sont restés. Les bits insérés (à gauche) valent tous 0. 6.2 Exemple Le résultat d'un ~ sur la variable $a retourne un strict complément à 1 de $a c'est à dire que chaque bit de $a qui était à 1 passe à 0 et réciproquement. 4.2 Exemple Donc si on a: $a=53; $b=~$a; $a=5; // 00000101 en binaire $b=$a>>2; $a vaudra 5 soit 00000101 en binaire $b vaudra 1 soit 00000001 en binaire Remarquez que chaque décalage correspond à une division entière par 2 de la valeur. $a vaudra 00000000 00000000 00000000 00110101 soit 53 $b vaudra 11111111 11111111 11111111 11001010 soit 54 5 Décalage à gauche (<<) 5.1 Introduction Cet opérateur permet de décaler les bits d'une variable n fois vers la gauche. Le résultat de l'opération correspond non pas aux bits qui sont sorti mais ceux qui sont restés. Les bits insérés (à droite) valent tous 0. Globalis Media Systems Siège social : 75, rue de Lourmel 75015 Paris. Bureaux et correspondance : 25, rue Thiboumery 75015 Paris infos@globalis ms.com http://www.globalis ms.com 08/16/02 2
Quelques exemples didactiques Laissons de coté (pour un temps) les considérations de performances et voyons des exemples un peu complexes d'utilisation de ces opérateurs. 1 Addition de deux entiers quelconques Rappel pour ceux qui ne sont pas des familiers de la base 2. Lorsqu'on effectue une addition à la main en base 10 (comme en primaire) on calcule de la façon suivante : 012 +129 =141 9+2=11 je pose 1 et je retiens 1 1+2+1(retenue)=4 0+1=1 on a alors le résultat escompté (141) En binaire le fonctionnement est a peu près similaire : 3+1 nous donne 0000 0011 (3) +0000 0001 (1) =0000 0100 (4) Le raisonnement est le suivant : 1+1=10 je pose 0 et je retiens 1, 1+0+1(retenue)=0 je pose 0 et je retiens 1, 0+0+1(retenue)=1 La fonction ci dessous effectue une addition des deux valeurs qui lui sont fournie en paramètres : plus(11,14 Appel de la fonction function plus($a,$b) $a==11 (0000 1011) $b==14 (0000 1110) $test = $a; $test==11 (0000 1011) $a ^= $b; $a==5 (0000 0101) $test==10 (0000 1010) // Où sont $test <<= 1; $test==20 (0001 0100) // Décalage des retenues $b = $test; $b==20 (0001 0100) $test est non null donc on reprend la boucle $test = $a; $test==5 (0001 0100) $a ^= $b; $a==17 (0001 0001) $test <<= 1; $test==4 (0000 0100) // Où sont $test==8 (0000 1000) // Décalage des retenues $b = $test; $b==8 (0000 1000) $test est non null donc on reprend la boucle $test = $a; $test==17 (0001 0100) $a ^= $b; $a==25 (0001 1001) $test <<= 1; $test==0 (0000 0000) // Où sont $test==0 (0000 0000) // Décalage des retenues $b = $test; $b==0 (0000 0000) $test==0 (il n'y a plus de retenues) donc on sort de la boucle return $a // ON renvoit $a donc 25 (11+14) function plus($a,$b) { do { $test = $a; $a ^= $b; // Où sont $test <<= 1; // Décalage des retenues $b = $test; // Propagation des retenues while ($test // retenues? return $a; Pour mieux comprendre le fonctionnement de la fonction suivons l'évolution des variables lors de l'appel de cette fonction avec les paramètres suivants $a=11 et $b=14 08/16/02 3
2 Multiplication de deux entiers quelconques Attaquons nous maintenant à la multiplication nous utiliserons lorsque nous en auront besoin la fonction plus($a,$b) que nous avons détaillé plus haut. function mul($a,$b) { $res = 0 ; $i = 0; while($b) { // Teste la valeur du bit de poids // le plus faible de $b if($b & 1) { $tmp = $a << $i; // $tmp=pow(2,$i $res=plus($res,$tmp // $res+=$tmp; // Décalage de $b en vue de tester //le bit suivant $b >>= 1; // Incrémentation de $i en vue de garder // une trace du poids du bit testé ensuite $i = plus($i,1 return $res; Le principe est le suivant, pour ne pas avoir à bouclé 6 fois sur $res+=3; algorithme certes fonctionnel mais bon le temps d'exécution serait proportionnel à la valeur du multiplicateur. Nous allons faire mieux. Pour cela nous allons chercher les puissances de 2 qui composent le multiplicateur ($b) multiplier $a par chacune de ces puissance de 2. Puis faire la sommes des résultats obtenus. Pour mieux comprendre le fonctionnement de la fonction, suivons l'évolution des variables lors de l'appel de cette fonction avec les paramètres suivants $a=3 et $b=6 mul(3,6 Appel de la fonction function mul($a,$b) $a==3 (0000 0011) if($b&1){ $tmp=$a<<$i; $res=plus($res,$tmp $b==1 $tmp=$i; $i=plus($i,1 if($b&1){ $tmp=$a<<$i; $b==3 (0000 0011) On reboucle $b&1 est vrai $a==3, $i==1, donc $tmp==6 $i==2 $res=plus($res,$tmp $res==18 $b>>=1; $i=plus($i,1 return $res; $b==1 (0000 0001) On reboucle $b&1 On rentre dans le if $a==3, $i==2, donc $tmp==12 $b==0 $i==3 $b==0 (0000 0000) On sort de la boucle On renvoie le résultat escompté 18 Quelques exemples concrets 1 Gestion d'articles sur deux colonnes Prenons comme premier exemple la problématique suivante. Imaginons que nous ayons une liste d'articles à afficher sur deux colonnes. Et que nous souhaitions équilibrer le nombre d'articles dans les deux colonnes. Dans notre exemple il y aura 11 articles, on souhaitera donc en avoir 6 dans la colonne de gauche et 5 dans celle de droite. Une approche classique serait la suivante : $nb_articles=11; $nb_a_gauche = $nb_articles/2; $nb_a_gauche = ceil($nb_a_gauche $nb_a_droite = $nb_articles $nb_a_gauche; $res=0 ; $b==6 (0000 0110) $res==0;(0000 0000) La nôtre toujours destinée à présenter les opérateurs sur les bits, sera sensiblement différente : $i=0; if ($b&1){ $i==0; (0000 0000) $b==6 (0000 0110) On entre dans la boucle $b & 1 est faux. On ne rentre pas dans le if $b >>= 1; $b==3 (0000 0011) Décalage de $b en vue de tester le bit de poids supérieur $b $i = plus($i,1 $i==1 $nb_articles=11; // permet de récupéré le résultat de la // division entière par 2 $nb_a_gauche = $nb_articles >> 1; // Permet de gérer le cas de d'un nombre impair if($nb_articles & 1) $nb_a_gauche++; $nb_a_droite = $nb_articles $nb_a_gauche; A titre d'information d'après les tests de performance que j'ai pu faire en bouclant 10000 fois sur chacun des deux scripts, j'ai observé que le second script mettait environ 10% de temps en moins pour s'exécuter. 08/16/02 4
2 Contrôle de droits d'accès d'un utilisateur Imaginons une problématique de contrôle d'accès à des pages. Problématique somme toute assez courante. Admettons que vous ayez quatre pages sur votre site WEB et un menu pour y accéder. Après authentification de votre utilisateur, vous récupérez ses droits d'accès. Vous pourriez stocker ses droits dans un fichier PHP contenant les informations suivantes et dont vous feriez une inclusion. // fichier de droits de l'utilisateur $page1= "OK"; $page2= "NON"; $page3= "OK"; $page4= "OK"; Puis traiter ses différentes chaînes pour afficher ou non le menu. En faisant des tests comme ceux ci. if($page1=="ok") echo "<a href=page1.php>page1</a><br>"; if($page2=="ok") echo "<a href=page2.php>page2</a><br>"; if($page3=="ok") echo "<a href=page3.php>page3</a><br>"; if($page4=="ok") echo "<a href=page4.php>page4</a><br>"; Mais vous pouvez aussi stocker ces informations d'une autre façon. // fichier de droits de l'utilisateur $droits_user =13; // 13 vaut 00001101 en binaire ce qui signifie dans // notre formalisme que l'utilisateur actuel a // le droit de voir les pages 1,3,4 Et traiter cette information de la façon suivante $masque=1; $i=1; for ($i=1;$i<5;$i++) { if ($droits_user & $masque) { echo "<a href=page$i.php>page$i</a><br>"; // Multiplication par 2 de $masque pour tester // si le bit suivant est à 1 $masque <<= 1; 3 Contrôle de droits avec gestion de groupe Maintenant imaginons que notre application est un peu plus compliquée et les utilisateurs obtiennent leur droits via les groupes auxquels ils appartiennent. L'utilisateur n'a donc plus de droits propres. // Groupes de l'utilisateur $groupe = array( 'redacteur'=> "OK", 'admin'=>"non", 'relecteur'="ok" // Droits rédacteur 'page1'=>"ok", 'page2'=>"non", 'page3'=>"ok", 'page4'=>"non" // Droits administrateur 'page1'=>"ok", 'page2'=>"ok", 'page3'=>"ok", 'page4'=>"ok" // Droits relecteur 'page1'=>"ok", 'page2'=>"non", 'page3'=>"non", 'page4'=>"ok" Notre Utilisateur a donc les droits de voir les pages 1,3,4. Vous pressentez ici que les tests qui vont permettre l'affichage des liens deviennent beaucoup plus complexes. Utilisons maintenant le même principe que précédemment. // Groupes de l'utilisateur $groupes = 5; // 00000101 $page_redacteur = 5; // Droits rédacteur $page_admin = 15; // Droits administrateur $page_relecteur=9; // Droits relecteur // On récupère les droits des groupes de l'utilisateur // en incluant les bons fichiers de droits, puis on // calcule les droits de l'utilisateur obtenu via // les groupes en faisant $droits_user=$page_redacteur; // 00000101 $droits_user =$page_relecteur; // 00000101 ou 00001001 // =00001101 // Enfin on affiche les pages correspondantes $masque = 1; $i = 1; for($i=1;$i<5;$i++) { if(droits_userecho "<a href=page$i.php>page$i</a><br>"; // Multiplication par 2 de $masque pour tester // si le bit suivant est à 1 $masque <<= 1; / Notre formalisme permet de récupérer, en très peu d'opérations, la résultante des droits de l'utilisateur et de l'exploiter de la même manière que précédemment. Conclusion Comme nous venons de le voir, il est possible de faire une utilisation intelligente des opérateurs sur les bits. Cependant, il est relativement difficile de faire une estimation du gain en terme de performances. Néenmoins, comme nous avons pu le constater dans les deux derniers exemples, ils présentent un intérêt fonctionnel réel. Ils ont aussi l'avantage de familiariser un tant soit peu les développeurs avec les couche basse de la programmation. olivier.huet@globalis ms.com 08/16/02 5