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

16

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

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

C'est juste un exemple; Je parle de la manipulation au niveau du bit en général. En C ++, y a-t-il vraiment un point? Ce qui précède est déformant et sujet aux erreurs, tandis que l'utilisation d'un std::bitsetvous permet de:

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

Ma question est; y a-t-il une raison de ne pas utiliser std::bitsetdes types primitifs, autres que pour la compatibilité descendante?

quant
la source
La taille de a std::bitsetest fixée au moment de la compilation. C'est le seul inconvénient paralysant auquel je peux penser.
rwong
1
@rwong Je parle de la std::bitsetmanipulation des bits de type c (par exemple int), qui est également corrigée au moment de la compilation.
quant
Une raison pourrait être le code hérité: le code a été écrit lorsqu'il std::bitsetn'était pas disponible (ou connu de l'auteur) et il n'y avait aucune raison de réécrire le code à utiliser std::bitset.
Bart van Ingen Schenau
Personnellement, je pense que la question de savoir comment rendre les "opérations sur un ensemble / carte / tableau de variables binaires" faciles à comprendre pour tout le monde n'est toujours pas résolue, car de nombreuses opérations utilisées dans la pratique ne peuvent pas être réduites à des opérations simples. Il existe également trop de façons de représenter de tels ensembles, dont bitsetun, mais un petit vecteur ou un ensemble de ints (d'index de bits) peut également être légitime. La philosophie de C / C ++ ne cache pas ces complexités de choix au programmeur.
rwong

Réponses:

12

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, toutes les "fonctions manquantes" pourraient être ajoutées par n'importe qui, et ne sont donc pas considérées 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::bitsetest une constante au moment de la compilation et que, même si elle n'est pas considérée comme un inconvénient, c'est toujours quelque chose qui affecte 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::bitsetcode l'emporte immédiatement sur le code C ordinaire. Il faut regarder de plus gros morceaux de code (et non un échantillon de jouet) pour dire si l'utilisation de std::bitseta 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, comme expliqué ci-dessous.


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, bitseta aussi d' autres opérateurs, tels que operator &=, operator <<=, etc., qui fonctionne sur toute la largeur de la bitset. É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-sujet sous manipulations de bits.


Certains fournisseurs C ++ peuvent implémenter bitsetentre 64 bits et 128 bits avec SIMD. Certains fournisseurs pourraient ne pas (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::bitseta des limites, je peux donner deux exemples.

  1. La taille de std::bitsetdoit être connue au moment de la compilation. Pour faire un tableau de bits avec une taille choisie dynamiquement, il faudra utiliser std::vector<bool>.
  2. La spécification C ++ actuelle pour std::bitsetne fournit pas un moyen d'extraire une tranche consécutive de N bits à partir d'un plus grand bitsetde 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 une sorte d'adaptateurs pour effectuer la tâche, même si la norme bitsetn'est pas extensible.


Il existe certains types d'opérations SWAR avancées qui ne sont pas fournis dès le départ std::bitset. On pourrait lire sur ces opérations sur ce site Web les permutations de bits . Comme d'habitude, on peut les implémenter par eux-mêmes, fonctionnant par dessus 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 sortie" (avec les optimisations activées), et assurez-vous que le code n'est pas éliminé (élimination du code mort) ou 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) faisait correctement les microbenchmarks, la seule façon de parvenir à une conclusion fiable est de faire nos propres microbenchmarks, de documenter les détails et de soumettre à l'examen et à la critique du public. Cela ne fait pas de mal de refaire des microbenchmarks que d'autres ont déjà fait.

rwong
la source
Le problème n ° 2 signifie également que le jeu de bits ne peut pas être utilisé dans une configuration parallèle où chaque thread doit fonctionner sur un sous-ensemble du jeu de bits.
user239558
@ user239558 Je doute que quelqu'un veuille paralléliser la même chose std::bitset. Il n'y a pas de garantie de cohérence de la mémoire (in std::bitset), ce qui signifie qu'elle n'est pas censée être partagée entre les cœurs. Les personnes qui ont besoin de le partager entre les cœurs auront tendance à créer leur propre implémentation. Lorsque les données sont partagées entre différents cœurs, il est habituel de les aligner sur la limite de la ligne de cache. Ne pas le faire réduit les performances et expose davantage les pièges de non-atomicité. Je n'ai pas suffisamment de connaissances pour donner un aperçu de la manière de construire une implémentation parallélisable de std::bitset.
rwong
la programmation parallèle de données ne nécessite généralement aucune cohérence de mémoire. vous synchronisez uniquement entre les phases. Je voudrais absolument traiter un jeu de bits en parallèle, je pense que toute personne ayant une grande bitsetvolonté.
user239558
@ user239558 qui ressemble à une copie implique (la plage de bits appropriée à traiter par chaque cœur doit être copiée avant le début du traitement). Je suis d'accord avec cela, même si je pense que quiconque pense à la parallélisation penserait déjà à déployer sa propre implémentation. En général, de nombreuses fonctionnalités de bibliothèque standard C ++ sont fournies en tant qu'implémentations de base; quiconque ayant des besoins plus sérieux va mettre en œuvre les leurs.
rwong
non il n'y a pas de copie. c'est simplement accéder à différentes parties d'une structure de données statique. aucune synchronisation n'est alors nécessaire.
user239558
2

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 , d'encodages intelligents entiers de positions de 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 |
}
David Zhang
la source
2
Pensez-vous qu'un std::bitsetserait plus lent?
quant
Eh bien, d'un coup d'œil rapide à la source, le jeu de bits libc ++ est basé sur une seule taille_t ou un tableau d'entre eux, donc se compilerait probablement vers quelque chose d'essentiellement équivalent / identique, en particulier sur un système où sizeof (size_t) == 8 - donc non, ce ne serait probablement pas plus lent.
Ryan Pavlik