web-dev-qa-db-fra.com

Y a-t-il un avantage à la manipulation de bits de style c par rapport à std :: bitset?

Je travaille presque exclusivement en C++ 11/14, et grince généralement quand je vois du code comme ceci:

std::int64_t mArray;
mArray |= someMask << 1;

C'est juste un exemple; Je parle de la manipulation bit à bit en général. En C++, y a-t-il vraiment un point? Ce qui précède est déformant et source d'erreurs, tout en utilisant un std::bitset vous permet de:

  1. modifier plus facilement la taille du std::bitset au besoin en ajustant un paramètre de modèle et en laissant l'implémentation s'occuper du reste, et
  2. passer moins de temps à comprendre ce qui se passe (et peut-être faire des erreurs) et écrire std::bitset d'une manière similaire à std::array ou d'autres conteneurs de données.

Ma question est; y a-t-il une raison pas d'utiliser std::bitset sur des types primitifs, autres que pour la compatibilité descendante?

17
quant

D'un point de vue logique (non technique), il n'y a aucun avantage.

Tout code C/C++ ordinaire peut être encapsulé dans une "construction de bibliothèque" appropriée. Après un tel emballage, la question de "si cela est plus avantageux que cela" devient une question théorique.

Du point de vue de la vitesse, C/C++ devrait permettre à la construction de la bibliothèque de générer du code aussi efficace que le code ordinaire qu'il encapsule. Ceci est cependant soumis à:

  • Fonction inlining
  • Vérification à la compilation et élimination de la vérification inutile de l'exécution
  • Élimination du code mort
  • Beaucoup d'autres optimisations de code ...

En utilisant ce type d'argument non technique, n'importe quelle "fonction manquante" pourrait être ajoutée par n'importe qui, et n'est donc pas considérée comme un inconvénient.

Cependant, les exigences et limitations intégrées ne peuvent pas être surmontées avec du code supplémentaire. Ci-dessous, je soutiens que la taille de std::bitset est une constante au moment de la compilation et, par conséquent, bien qu'elle ne soit pas considérée comme un inconvénient, elle affecte toujours le choix de l'utilisateur.


D'un point de vue esthétique (lisibilité, facilité d'entretien, etc.), il y a une différence.

Cependant, il n'est pas évident que le std::bitset le code l'emporte immédiatement sur le simple code C. Il faut regarder de plus gros morceaux de code (et non un échantillon de jouet) pour dire si l'utilisation de std::bitset a amélioré la qualité humaine du code source.


La vitesse de manipulation des bits dépend du style de codage. Le style de codage affecte à la fois la manipulation des bits C/C++ et s'applique également à std::bitset également, comme expliqué ci-après.


Si l'on écrit du code qui utilise le operator [] pour lire et écrire un bit à la fois, il faudra le faire plusieurs fois s'il y a plus d'un bit à manipuler. La même chose peut être dite du code de style C.

Cependant, bitset possède également d'autres opérateurs, tels que operator &=, operator <<=, etc., qui fonctionne sur toute la largeur du jeu de bits. Étant donné que la machine sous-jacente peut souvent fonctionner sur 32 bits, 64 bits et parfois 128 bits (avec SIMD) à la fois (dans le même nombre de cycles de processeur), un code conçu pour tirer parti de ces opérations multi-bits peut être plus rapide que le code de manipulation de bits "en boucle".

L'idée générale est appelée SWAR (SIMD dans un registre) , et est un sous-thème sous les manipulations de bits.


Certains fournisseurs C++ peuvent implémenter bitset entre 64 bits et 128 bits avec SIMD. Certains fournisseurs pourraient ne pas le faire (mais pourraient éventuellement le faire). S'il est nécessaire de savoir ce que fait la bibliothèque du fournisseur C++, la seule façon est de regarder le désassemblage.


Quant à savoir si std::bitset a des limites, je peux donner deux exemples.

  1. La taille de std::bitset doit être connu au moment de la compilation. Pour créer un tableau de bits avec une taille choisie dynamiquement, il faudra utiliser std::vector<bool>.
  2. La spécification C++ actuelle pour std::bitset ne permet pas d'extraire une tranche consécutive de N bits d'un bitset plus grand de M bits.

La première est fondamentale, ce qui signifie que pour les personnes qui ont besoin de bitsets de taille dynamique, elles doivent choisir d'autres options.

Le second peut être surmonté, car on peut écrire ne sorte d'adaptateurs pour effectuer la tâche, même si la norme bitset n'est pas extensible.


Il existe certains types d'opérations SWAR avancées qui ne sont pas fournis dès le départ de std::bitset. On pourrait lire sur ces opérations sur ce site sur les permutations de bits . Comme d'habitude, on peut les implémenter par eux-mêmes, fonctionnant en plus de std::bitset.


Concernant la discussion sur la performance.

Un avertissement: beaucoup de gens demandent pourquoi (quelque chose) de la bibliothèque standard est beaucoup plus lent qu'un simple code de style C. Je ne répéterais pas ici les connaissances préalables en matière de micro-analyse comparative, mais j'ai juste ce conseil: assurez-vous de comparer en "mode de libération" (avec les optimisations activées), et assurez-vous que le code n'est pas éliminé (code mort) élimination) ou être hissé hors d'une boucle (mouvement de code invariant en boucle) .

Étant donné qu'en général, nous ne pouvons pas dire si quelqu'un (sur Internet) a correctement effectué les tests de performance, la seule façon d'obtenir une conclusion fiable est de faire nos propres tests de performance, de documenter les détails et de les soumettre à l'examen et à la critique du public. Cela ne fait pas de mal de refaire des microbenchmarks que d'autres ont fait auparavant.

12
rwong

Cela ne s'applique certainement pas dans tous les cas, mais parfois un algorithme peut dépendre de l'efficacité du bit-twiddling de style C pour fournir des gains de performances significatifs. Le premier exemple qui me vient à l'esprit est l'utilisation de bitboards , des encodages entiers intelligents des positions des jeux de société, pour accélérer les moteurs d'échecs et autres. Ici, la taille fixe des types entiers ne pose aucun problème, car les échiquiers sont toujours 8 * 8 de toute façon.

Pour un exemple simple, considérons la fonction suivante (tirée de cette réponse de Ben Jackson ) qui teste une position Connect Four pour la victoire:

// return whether newboard includes a win
bool haswon2(uint64_t newboard)
{
    uint64_t y = newboard & (newboard >> 6);
    uint64_t z = newboard & (newboard >> 7);
    uint64_t w = newboard & (newboard >> 8);
    uint64_t x = newboard & (newboard >> 1);
    return (y & (y >> 2 * 6)) | // check \ diagonal
           (z & (z >> 2 * 7)) | // check horizontal -
           (w & (w >> 2 * 8)) | // check / diagonal
           (x & (x >> 2));      // check vertical |
}
2
David Zhang