web-dev-qa-db-fra.com

Quand est-il utile d'utiliser des champs de bits?

Vaut-il la peine d'utiliser l'implémentation du champ binaire de C? Si oui, quand est-il utilisé?

Je regardais un code d'émulateur et il semble que les registres des puces ne soient pas implémentés à l'aide de champs de bits.

Est-ce quelque chose qui est évité pour des raisons de performances (ou pour une autre raison)?

Y a-t-il encore des moments où des champs binaires sont utilisés? (c.-à-d. firmware pour mettre des puces réelles, etc.)

49
Russel

Les champs de bits ne sont généralement utilisés que lorsqu'il est nécessaire de mapper des champs de structure à des tranches de bits spécifiques, où du matériel interprétera les bits bruts. Un exemple pourrait être l'assemblage d'un en-tête de paquet IP. Je ne vois pas de raison impérieuse pour qu'un émulateur modélise un registre à l'aide de champs binaires, car il ne touchera jamais le vrai matériel!

Bien que les champs binaires puissent conduire à une syntaxe soignée, ils sont assez dépendants de la plate-forme et donc non portables. Une approche plus portable, mais encore plus verbeuse, consiste à utiliser la manipulation directe au niveau du bit, en utilisant des décalages et des masques de bits.

Si vous utilisez des champs binaires pour autre chose que l'assemblage (ou le désassemblage) de structures sur une interface physique, les performances peuvent en souffrir. En effet, chaque fois que vous lisez ou écrivez à partir d'un champ binaire, le compilateur devra générer du code pour effectuer le masquage et le décalage, ce qui brûlera les cycles.

24

Une utilisation pour les champs de bits qui n'a pas encore été mentionnée est que les champs de bits unsigned fournissent un module arithmétique une puissance de deux "gratuitement". Par exemple, étant donné:

struct { unsigned x:10; } foo;

l'arithmétique sur foo.x sera effectuée modulo 2dix = 1024.

(La même chose peut être obtenue directement en utilisant des opérations au niveau du bit &, Bien sûr - mais parfois cela peut conduire à un code plus clair pour que le compilateur le fasse pour vous).

19
caf

FWIW, et en ne regardant que la question de la performance relative - une référence complexe:

#include <time.h>
#include <iostream>

struct A
{
    void a(unsigned n) { a_ = n; }
    void b(unsigned n) { b_ = n; }
    void c(unsigned n) { c_ = n; }
    void d(unsigned n) { d_ = n; }
    unsigned a() { return a_; }
    unsigned b() { return b_; }
    unsigned c() { return c_; }
    unsigned d() { return d_; }
    volatile unsigned a_:1,
                      b_:5,
                      c_:2,
                      d_:8;
};

struct B
{
    void a(unsigned n) { a_ = n; }
    void b(unsigned n) { b_ = n; }
    void c(unsigned n) { c_ = n; }
    void d(unsigned n) { d_ = n; }
    unsigned a() { return a_; }
    unsigned b() { return b_; }
    unsigned c() { return c_; }
    unsigned d() { return d_; }
    volatile unsigned a_, b_, c_, d_;
};

struct C
{
    void a(unsigned n) { x_ &= ~0x01; x_ |= n; }
    void b(unsigned n) { x_ &= ~0x3E; x_ |= n << 1; }
    void c(unsigned n) { x_ &= ~0xC0; x_ |= n << 6; }
    void d(unsigned n) { x_ &= ~0xFF00; x_ |= n << 8; }
    unsigned a() const { return x_ & 0x01; }
    unsigned b() const { return (x_ & 0x3E) >> 1; }
    unsigned c() const { return (x_ & 0xC0) >> 6; }
    unsigned d() const { return (x_ & 0xFF00) >> 8; }
    volatile unsigned x_;
};

struct Timer
{
    Timer() { get(&start_tp); }
    double elapsed() const {
        struct timespec end_tp;
        get(&end_tp);
        return (end_tp.tv_sec - start_tp.tv_sec) +
               (1E-9 * end_tp.tv_nsec - 1E-9 * start_tp.tv_nsec);
    }
  private:
    static void get(struct timespec* p_tp) {
        if (clock_gettime(CLOCK_REALTIME, p_tp) != 0)
        {
            std::cerr << "clock_gettime() error\n";
            exit(EXIT_FAILURE);
        }
    }
    struct timespec start_tp;
};

template <typename T>
unsigned f()
{
    int n = 0;
    Timer timer;
    T t;
    for (int i = 0; i < 10000000; ++i)
    {
        t.a(i & 0x01);
        t.b(i & 0x1F);
        t.c(i & 0x03);
        t.d(i & 0xFF);
        n += t.a() + t.b() + t.c() + t.d();
    }
    std::cout << timer.elapsed() << '\n';
    return n;
}

int main()
{
    std::cout << "bitfields: " << f<A>() << '\n';
    std::cout << "separate ints: " << f<B>() << '\n';
    std::cout << "explicit and/or/shift: " << f<C>() << '\n';
}

Sortie sur ma machine de test (les nombres varient de ~ 20% pour exécuter):

bitfields: 0.140586
1449991808
separate ints: 0.039374
1449991808
explicit and/or/shift: 0.252723
1449991808

Suggère qu'avec g ++ -O3 sur un Athlon assez récent, les champs de bits sont pires que quelques fois plus lents que les entiers séparés, et cette implémentation particulière et/ou/bitshift est au moins deux fois plus mauvaise ("pire" que d'autres opérations comme la lecture de mémoire/les écritures sont accentuées par la volatilité ci-dessus, et il y a un overhead de boucle, etc., donc les différences sont sous-estimées dans les résultats).

Si vous traitez des centaines de mégaoctets de structures qui peuvent être principalement des champs de bits ou des entiers principalement distincts, les problèmes de mise en cache peuvent devenir dominants - alors référence dans votre système.


MISE À JOUR: l'utilisateur2188211 a tenté une modification qui a été rejetée mais a illustré utilement comment les champs de bits deviennent plus rapides à mesure que la quantité de données augmente: "lors de l'itération sur un vecteur de quelques millions d'éléments dans [une version modifiée de] le code ci-dessus, de sorte que les variables ne ne réside pas dans le cache ou les registres, le code de champ binaire peut être le plus rapide. "

template <typename T>
unsigned f()
{
    int n = 0;
    Timer timer;
    std::vector<T> ts(1024 * 1024 * 16);
    for (size_t i = 0, idx = 0; i < 10000000; ++i)
    {
        T& t = ts[idx];
        t.a(i & 0x01);
        t.b(i & 0x1F);
        t.c(i & 0x03);
        t.d(i & 0xFF);
        n += t.a() + t.b() + t.c() + t.d();
        idx++;
        if (idx >= ts.size()) {
            idx = 0;
        }
    }
    std::cout << timer.elapsed() << '\n';
    return n;
}

Résultats d'un exemple d'exécution (g ++ -03, Core2Duo):

 0.19016
 bitfields: 1449991808
 0.342756
 separate ints: 1449991808
 0.215243
 explicit and/or/shift: 1449991808

Bien sûr, le timing est tout relatif et la façon dont vous implémentez ces champs peut ne pas avoir d'importance du tout dans le contexte de votre système.

9
Tony Delroy

J'ai vu/utilisé des champs de bits dans deux situations: jeux informatiques et interfaces matérielles. L'utilisation du matériel est assez simple: le matériel attend des données dans un certain format de bits que vous pouvez définir manuellement ou via des structures de bibliothèque prédéfinies. Cela dépend de la bibliothèque spécifique, qu'ils utilisent des champs de bits ou simplement une manipulation de bits.

Dans les "vieux temps", les jeux informatiques utilisaient fréquemment les champs de bits pour exploiter au maximum la mémoire de l'ordinateur/du disque. Par exemple, pour une définition NPC dans un RPG, vous pouvez trouver (exemple composé):

struct charinfo_t
{
     unsigned int Strength : 7;  // 0-100
     unsigned int Agility : 7;  
     unsigned int Endurance: 7;  
     unsigned int Speed : 7;  
     unsigned int Charisma : 7;  
     unsigned int HitPoints : 10;    //0-1000
     unsigned int MaxHitPoints : 10;  
     //etc...
};

Vous ne le voyez pas tellement dans les jeux/logiciels plus modernes car les économies d'espace se sont aggravées proportionnellement à mesure que les ordinateurs disposent de plus de mémoire. Enregistrer 1 Mo de mémoire lorsque votre ordinateur ne dispose que de 16 Mo est un gros problème, mais pas tant que vous avez 4 Go.

7
uesp

Le but principal des champs binaires est de fournir un moyen d'économiser de la mémoire dans des structures de données agrégées massivement instanciées en réalisant un compactage plus serré des données.

L'idée est de tirer parti des situations où vous avez plusieurs champs dans un type de structure, qui n'ont pas besoin de toute la largeur (et la plage) d'un type de données standard. Cela vous donne la possibilité de regrouper plusieurs de ces champs dans une seule unité d'allocation, réduisant ainsi la taille globale du type de structure. Et un exemple extrême serait les champs booléens, qui peuvent être représentés par des bits individuels (avec, disons, 32 d'entre eux étant compressables en un seul unsigned int unité d'allocation).

De toute évidence, cela n'a de sens que dans les situations où les avantages de la consommation réduite de mémoire l'emportent sur les inconvénients d'un accès plus lent aux valeurs stockées dans les champs binaires. Cependant, de telles situations se produisent assez souvent, ce qui fait des champs binaires une fonctionnalité de langage absolument indispensable. Cela devrait répondre à votre question sur l'utilisation moderne des champs de bits: non seulement ils sont utilisés, ils sont essentiellement obligatoires dans tout code pratiquement significatif orienté sur le traitement de grandes quantités de données homogènes (comme les grands graphiques, par exemple), car leur mémoire -les avantages d'économies l'emportent largement sur les pénalités de performances d'accès individuel.

D'une certaine manière, les champs de bits dans leur fonction sont très similaires à des choses comme les "petits" types arithmétiques: signed/unsigned char, short, float. Dans le code informatique réel, on n'utiliserait normalement aucun type plus petit que int ou double (à quelques exceptions près). Types arithmétiques comme signed/unsigned char, short, float existent uniquement pour servir de types de "stockage": en tant que membres compacts économes en mémoire de types struct dans des situations où leur plage (ou précision) est connue pour être suffisante. Les champs binaires sont juste une autre étape dans la même direction, qui échange un peu plus de performances pour des avantages d'économie de mémoire beaucoup plus importants.

Donc, cela nous donne un ensemble assez clair de conditions dans lesquelles il vaut la peine d'employer des champs binaires:

  1. Le type de structure contient plusieurs champs qui peuvent être regroupés en un plus petit nombre de bits.
  2. Le programme instancie un grand nombre d'objets de ce type struct.

Si les conditions sont remplies, vous déclarez tous les champs compressables de façon contiguë (généralement à la fin du type de structure), leur affectez leurs largeurs de bits appropriées (et, généralement, prenez certaines mesures pour vous assurer que les largeurs de bits sont appropriées) . Dans la plupart des cas, il est logique de jouer avec la commande de ces champs pour obtenir le meilleur emballage et/ou les meilleures performances.


Il existe également une étrange utilisation secondaire des champs de bits: les utiliser pour mapper des groupes de bits dans diverses représentations spécifiées en externe, comme les registres matériels, les formats à virgule flottante, les formats de fichiers, etc. Cela n'a jamais été conçu comme une utilisation appropriée des champs de bits , même si, pour une raison inexpliquée, ce type d'abus de champ binaire continue d'apparaître dans le code réel. Ne fais pas ça.

3
AnT

Les champs de bits étaient utilisés dans les anciens jours pour enregistrer la mémoire du programme.

Ils dégradent les performances car les registres ne peuvent pas fonctionner avec eux, ils doivent donc être convertis en entiers pour faire quoi que ce soit avec eux. Ils ont tendance à conduire à un code plus complexe qui n'est pas transférable et plus difficile à comprendre (car il faut tout le temps masquer et démasquer les choses pour réellement utiliser les valeurs.)

Consultez la source de http://www.nethack.org/ pour voir le pré ansi c dans toute sa splendeur de champ de bits!

2
nate c

Dans les années 70, j'utilisais des champs de bits pour contrôler le matériel sur un très80. L'affichage/le clavier/la cassette/les disques étaient tous des périphériques mappés en mémoire. Les bits individuels contrôlaient diverses choses.

  1. Un affichage 32 colonnes vs 64 colonnes contrôlé par bit.
  2. Le bit 0 dans cette même cellule de mémoire correspondait aux entrées/sorties série des cassettes.

Si je me souviens bien, le contrôle du lecteur de disque en avait plusieurs. Il y avait 4 octets au total. Je pense qu'il y avait une sélection de lecteur 2 bits. Mais c'était il y a longtemps. C'était assez impressionnant à l'époque, car il y avait au moins deux compilateurs c différents pour la forme de la plante.

L'autre observation est que les champs de bits sont vraiment spécifiques à la plate-forme. Il n'est pas prévu qu'un programme avec des champs de bits soit porté sur une autre plate-forme.

1
EvilTeach

Une des utilisations des champs de bits était de refléter les registres matériels lors de l'écriture de code incorporé. Cependant, puisque l'ordre des bits dépend de la plate-forme, ils ne fonctionnent pas si le matériel commande ses bits différents du processeur. Cela dit, je ne peux plus penser à une utilisation des champs de bits. Vous feriez mieux d'implémenter une bibliothèque de manipulation de bits qui peut être portée sur plusieurs plateformes.

1
sizzzzlerz

Dans le code moderne, il n'y a vraiment qu'une seule raison d'utiliser des champs de bits: pour contrôler les exigences d'espace d'un type bool ou enum, au sein d'une structure/classe. Par exemple (C++):

enum token_code { TK_a, TK_b, TK_c, ... /* less than 255 codes */ };
struct token {
    token_code code      : 8;
    bool number_unsigned : 1;
    bool is_keyword      : 1;
    /* etc */
};

OMI, il n'y a fondamentalement aucune raison de ne pas utiliser :1 champs de bits pour bool, car les compilateurs modernes généreront du code très efficace pour cela. En C, cependant, assurez-vous que votre typedef bool est bien le C99 _Bool ou à défaut d'un non signé int, car un champ 1 bit signé ne peut contenir que les valeurs 0 et -1 (à moins que vous n'ayez en quelque sorte une machine non complémentaire de deux).

Avec les types d'énumération, utilisez toujours une taille qui correspond à la taille de l'un des types entiers primitifs (8/16/32/64 bits, sur les processeurs normaux) pour éviter la génération de code inefficace (cycles de lecture-modification-écriture répétés, généralement) .

L'utilisation de champs de bits pour aligner une structure avec un format de données défini en externe (en-têtes de paquet, registres d'E/S mappés en mémoire) est couramment suggérée, mais je considère en fait que c'est une mauvaise pratique, car C ne vous donne pas assez de contrôle sur l'endianité , padding et (pour les régulations d'E/S) exactement quelles séquences d'assemblage sont émises. Jetez un coup d'œil aux clauses de représentation d'Ada si vous voulez voir combien de C manque dans cette zone.

1
zwol

Boost.Thread utilise des champs de bits dans son shared_mutex, sur Windows au moins:

    struct state_data
    {
        unsigned shared_count:11,
        shared_waiting:11,
        exclusive:1,
        upgrade:1,
        exclusive_waiting:7,
        exclusive_waiting_blocked:1;
    };
0
Steve Townsend