2010-11-21 14 views
32

¿Vale la pena utilizar la implementación del campo de bits de C? Si es así, ¿cuándo se usa?¿Cuándo vale la pena usar los campos de bits?

Estaba mirando a través de un código de emulador y parece que los registros de los chips no se están implementando con campos de bits.

¿Esto es algo que se evita por motivos de rendimiento (u otra razón)?

¿Hay momentos en los que se utilizan campos de bits? (es decir, firmware para colocar chips reales, etc.)

Respuesta

18

Los campos de bits normalmente solo se usan cuando existe la necesidad de asignar campos de estructura a segmentos de bits específicos, donde algunos hardware interpretarán los bits sin formato. Un ejemplo podría ser ensamblar un encabezado de paquete IP. No veo una razón convincente para que un emulador modele un registro usando campos de bits, ¡ya que nunca tocará el hardware real!

Mientras que los campos de bits pueden llevar a una sintaxis clara, son bastante dependientes de la plataforma, y ​​por lo tanto no portátiles. Un enfoque más portátil, pero aún más detallado, es utilizar la manipulación directa a través de bits, utilizando turnos y máscaras de bits.

Si utiliza campos de bits para cualquier cosa que no sea ensamblar (o desmontar) estructuras en alguna interfaz física, el rendimiento puede verse afectado. Esto se debe a que cada vez que lee o escribe desde un campo de bits, el compilador tendrá que generar un código para realizar el enmascaramiento y el desplazamiento, que grabará ciclos.

+1

en relación con el problema de "ciclos de grabación", he descubierto que de hecho era más rápido usar el tipo entero más pequeño posible en lugar de utilizar campos de bits. A excepción de las banderas booleanas (para las cuales el enmascaramiento es fácil y no se requieren cambios), por lo tanto estoy de acuerdo contigo :) –

+0

@Matthieu: Me imagino que en * la mayoría de las circunstancias, usar 'int' sería más rápido, porque es la plataforma ancho nativo La excepción a esto sería si hacer todo como 'int' hace que sus estructuras de datos sean significativamente más grandes, causando fallas en la memoria caché, etc. –

+1

@OliCharlesworth, el problema de la red little-endian o big-endian hará que use el encabezado del paquete de paso de bit-campo ha fallado. Y el estándar de C++ tampoco define cómo se almacena el campo de bits, sino que es específico de la implementación. Y la base en el rendimiento del campo de bits no es buena, el campo de bits es inútil. – ZijingWu

1

Un uso para los campos de bit solía reflejar registros de hardware al escribir código incrustado. Sin embargo, dado que el orden de los bits depende de la plataforma, no funcionan si el hardware ordena sus bits diferentes del procesador. Dicho esto, no puedo pensar en un uso para campos de bit más. Es mejor implementar una biblioteca de manipulación de bits que pueda ser portada a través de plataformas.

+0

¿Cómo se debe escribir mejor? ¿una biblioteca? Un enfoque en C++ sería definir una propiedad que tomaría una dirección y un rango de bit y arrojaría un valor lvalue modificable, y luego haría algo como "#define UART3_BAUD_RATE_MODE MOD_BITFIELD (UART3_CONTROL, 12,4)". Me gustaría esperar que uno pueda arreglar las cosas para que las lecturas y escrituras estén en línea (en lugar de generar llamadas a funciones) pero no sé cómo hacer para que los operadores de asignación lógica-lógica booleana (| = etc.) funcionen de manera eficiente y/o atómicamente – supercat

2

Los campos de bits se usaron en el antiguos días para guardar la memoria del programa.

Degradan el rendimiento porque los registros no pueden funcionar con ellos, por lo que deben convertirse en enteros para hacer cualquier cosa con ellos. Tienden a conducir a un código más complejo que es inservible y más difícil de entender (ya que tiene que enmascarar y desenmascarar cosas todo el tiempo para usar realmente los valores).

Consulte la fuente de http://www.nethack.org/ para ver pre ansi c in toda su gloria bitfield!

5

He visto/usado campos de bits en dos situaciones: juegos de computadora e interfaces de hardware. El uso del hardware es bastante directo: el hardware espera datos en un formato de cierto bit que puede definir manualmente o mediante estructuras de biblioteca predefinidas. Depende de la biblioteca específica si usan campos de bits o simplemente manipulación de bits.

En los "viejos tiempos" los juegos de computadoras utilizaban campos de bits con frecuencia para aprovechar al máximo la memoria de la computadora/disco como sea posible. Por ejemplo, para una definición de la APN en un juego de rol que podrían encontrar (formada por ejemplo):

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... 
}; 

que no se ve tanto en los juegos más moderno/software como el ahorro de espacio ha conseguido proporcionalmente peor a medida que los ordenadores obtener más memoria. Guardar un 1MB de memoria cuando su computadora solo tiene 16MB es un gran problema, pero no tanto cuando tiene 4GB.

+2

Las computadoras pueden tener más RAM en estos días, pero mantener el uso de memoria más bajo puede ayudar a mantenerlo dentro de la memoria caché de la CPU, lo que aumentaría el rendimiento. Por otro lado, los campos de bits requieren más instrucciones para acceder a ellos, lo que reduce el rendimiento. ¿Cuál es más significativo? –

+0

@CraigMcQueen ¿Más instrucciones que qué? ¿Más que si los campos no fueran campos de bits o más que la lógica aritmética equivalente para mantenerlos dentro de un rango específico? Si es el primero, esa no es realmente una buena comparación. – Pharap

+0

@Pharap: Sí, comparando las instrucciones necesarias para leer/escribir bitfields vs plain 'int' struct members. Vale la pena considerarlo en el código apuntando a una alta velocidad de ejecución más que a un ahorro de memoria. –

0

Boost.Hilo utiliza campos de bits en su shared_mutex, en Windows, al menos:

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

Parece que esto se hizo para empaquetar la estructura en exactamente una palabra de máquina de 32 bits, probablemente por atomicidad. Boost folken sabe * exactamente * qué es lo que está haciendo y no suele explicarse con suficiente detalle para las personas que no lo hacen, lo que lamentablemente significa que copiar la lógica de Boost puede terminar fácilmente en lágrimas. Por ejemplo, hay una razón para ' exclusive' y 'upgrade' no son' bool', pero ¿sabes qué es? – zwol

+0

La lógica usa compare-and-swap para evitar bloqueos de kernel a menos que sea esencial. Estoy seguro de que es por eso que se hace de esta manera. –

1

En el código moderna, no hay realmente una sola razón para utilizar campos de bits: para controlar los requisitos de espacio de un tipo bool o un enum, dentro de una estructura/clase . Por ejemplo (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 Básicamente no hay razón para no usar :1 campos de bits para bool, como compiladores modernos van a generar código muy eficiente para él. En C, sin embargo, asegúrese de que su bool typedef es o bien el C99 _Bool o en su defecto un sin firmar int, debido a que un campo de 1 bit firmado puede contener sólo los valores 0 y -1 (a menos que de alguna manera no es marca de complemento a dos máquina).

Con tipos de enumeración, utilice siempre un tamaño que corresponda al tamaño de uno de los tipos enteros primitivos (8/16/32/64 bits, en CPU normales) para evitar la generación ineficaz de código (repetición de lectura-modificación-escritura ciclos, por lo general).

El uso de bitfields para alinear una estructura con algún formato de datos definido externamente (encabezados de paquetes, registros de E/S mapeados en memoria) es comúnmente sugerido, pero en realidad lo considero una mala práctica, porque C no te da suficiente control sobre endianness, padding, y (para I/O regs) exactamente qué secuencias de ensamble se emiten. Eche un vistazo a las cláusulas de representación de Ada en algún momento si desea ver cuánto falta C en esta área.

+1

Nota: los campos de bits en 'bool' y MSVC no se mezclan, para compatibilidad con MSVC debe usar algunos' unsigned'. –

+1

Otra razón más para evitar MSVC es :-( – zwol

+1

@Matthieu M .: Hmm .... En mi experiencia MSVC nunca tuvo ningún problema con el uso de cualquier tipo de enteros con campos de bits. – AnT

13

Un uso para campos de bit que aún no se ha mencionado es que unsigned bitfields proporcionan módulo aritmético una potencia de dos "gratis". Por ejemplo, dada:

struct { unsigned x:10; } foo; 

aritmética sobre foo.x se llevará a cabo en módulo 2 = 1024.

(El mismo se puede conseguir directamente mediante el uso de bit a bit & operaciones, por supuesto - pero a veces puede conduzca a un código más claro para que el compilador lo haga por usted).

+2

Vale la pena señalar que es probable que aquí 'sizeof (foo) == sizeof (unsigned)', es decir, no estás guardando ningún recuerdo, solo tienes una sintaxis más agradable. –

+2

No asumiría que 'sizeof (foo) == sizeof (unsigned)' cuando 'sizeof (unsigned) == 4' – MSalters

+0

@caf, también puede hacerlo sin bitfields sin dos problemas. Simplemente calcule el resultado y' & (2^n - 1) ' – ZijingWu

6

Fwiw, y mirar sólo a la cuestión rendimiento relativo - un punto de referencia bodgy:

#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'; 
} 

salida en mi máquina de prueba (los números varían según el ~ 20% de ejecución en ejecución):

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

Sugiere que con g ++ -O3 en un Athlon bastante reciente, los bitfields son peores que unas pocas veces más lentos que los ints separados, y esta implementación particular y/o/bitshift es al menos dos veces peor ("peor" que otras operaciones como memory read/las escrituras se enfatizan por la volatilidad anterior, y hay un ciclo sobrecarga, etc., por lo que las diferencias son subestimadas en los resultados).

Si se trata de cientos de megabytes de estructuras que pueden ser principalmente bitfields o ints principalmente distintos, los problemas de caché pueden volverse dominantes, por lo que el benchmark en su sistema.


ACTUALIZACIÓN: user2188211 intentó una edición que fue rechazada, pero útilmente ilustra la forma en campos de bits son más rápidos como la cantidad de datos aumenta: "cuando la iteración en un vector de unos pocos millones de elementos en [una versión modificada de] los anteriores código, de modo que las variables no residen en caché o registros, el código del campo de bits puede ser el más rápido ".

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; 
} 

Resultados sobre de un ejemplo de ejecución (g ++ -03, Core2Duo):

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

Por supuesto, de momento toda forma relativa, y que se implementa estos campos puede no importar en absoluto en el contexto de tu sistema

+0

En muchas plataformas, las estructuras de campo de bit y la estructura personalizada de máscara y desplazamiento son de 4 bytes de tamaño mientras que la estructura no bitfield es de 16 bytes. Así que el rendimiento/tamaño es aproximadamente igual para el campo de bits y la implementación por separado (es una compensación para usted decidir) mientras que la relación rendimiento/tamaño de la máscara personalizada y estructura de desplazamiento es menor que las otras, pero al menos es independiente de la plataforma. –

1

En los años 70 utilicé campos de bits para controlar el hardware en un trs80. La pantalla/teclado/cassette/discos eran todos dispositivos mapeados en la memoria. Los bits individuales controlaban varias cosas.

  1. Un bit controlado 32 columna vs 64 columna de visualización.
  2. El bit 0 en esa misma celda de memoria era la entrada/salida de datos seriales del cassette.

Como recuerdo, el control de la unidad de disco tenía varios de ellos. Hubo 4 bytes en total. Creo que hubo una selección de unidad de 2 bits. Pero fue hace mucho tiempo. Fue impresionante en aquel entonces que había al menos dos compiladores de c diferentes para la planta.

La otra observación es que los campos de bits realmente son específicos de la plataforma. No existe la expectativa de que un programa con campos de bits se transfiera a otra plataforma.

+0

Estoy de alguna manera sorprendido de que haya incluso un compilador de C para el TRS80 en los años 70 –

+0

Dos de ellos en realidad, aunque en su mayoría lo hice a mano con código z80. – EvilTeach

-2

una alternativa a considerar es especificar las estructuras de bits con una estructura ficticia (nunca instanciado), donde cada byte representa un poco:

struct Bf_format 
{ 
    char field1[5]; 
    char field2[9]; 
    char field3[18]; 
}; 

Con este enfoque sizeof da el ancho del campo de bits, y offsetof dan el desplazamiento del campo de bit. Al menos en el caso de GNU gcc, la optimización del compilador de operaciones de bits (con cambios constantes y máscaras) parece haber llegado a la paridad aproximada con los campos de bits (lenguaje base).

He escrito un archivo de encabezado C++ (usando este enfoque) que permite que las estructuras de los campos de bits se definan y utilicen de manera performante, mucho más portátil, mucho más flexible: https://github.com/wkaras/C-plus-plus-library-bit-fields. Entonces, a menos que esté atascado usando C, creo que rara vez habría una buena razón para usar la función de lenguaje base para los campos de bits.

0

El objetivo principal de los campos de bits es proporcionar una forma de ahorrar memoria en estructuras de datos agregadas creadas masivamente logrando un empaquetado de datos más ajustado.

La idea es aprovechar las situaciones en las que tiene varios campos en algún tipo de estructura, que no necesitan todo el ancho (y rango) de algún tipo de datos estándar. Esto le brinda la oportunidad de empaquetar varios de dichos campos en una unidad de asignación, reduciendo así el tamaño total del tipo de estructura. Y el ejemplo extremo serían los campos booleanos, que pueden representarse por bits individuales (con, por ejemplo, 32 de ellos que se pueden empaquetar en una sola unidad de asignación unsigned int).

Obviamente, esto solo tiene sentido en situaciones en las que las ventajas del consumo reducido de memoria superan los inconvenientes de un acceso más lento a los valores almacenados en campos de bits. Sin embargo, tales situaciones surgen con bastante frecuencia, lo que hace que los campos de bits sean una característica del lenguaje absolutamente indispensable. Esto debería responder a su pregunta sobre el uso moderno de los campos de bits: no solo se usan, sino que son esencialmente obligatorios en cualquier código práctico orientado al procesamiento de grandes cantidades de datos homogéneos (como gráficos grandes, por ejemplo), porque su memoria -los beneficios de ahorro superan con creces cualquier penalización de rendimiento de acceso individual.

En cierto modo, los campos de bits en su propósito son muy similares a los tipos aritméticos "pequeños": signed/unsigned char, short, float. En el código de procesamiento de datos real uno normalmente no usaría ningún tipo más pequeño que int o double (con algunas excepciones). Los tipos aritméticos como signed/unsigned char, short, float existen solo para servir como tipos de "almacenamiento": como miembros compactos que ahorran memoria de tipos de estructuras en situaciones donde se sabe que su rango (o precisión) es suficiente. Los campos de bits son solo un paso más en la misma dirección, que intercambia un poco más de rendimiento para obtener beneficios de ahorro de memoria mucho mayores.

Por lo tanto, eso nos da un conjunto bastante clara de las condiciones bajo las cuales vale la pena emplear campos de bits:

  1. tipo de estructura contiene múltiples campos que pueden ser empaquetados en un número menor de bits.
  2. El programa crea una gran cantidad de objetos de ese tipo de estructura.

Si se cumplen las condiciones, usted declara todos los campos de bits-empacable contigua (por lo general al final del tipo struct), asignarles sus bits anchuras adecuadas (y, por lo general, tomar algunas medidas para asegurar que el el ancho de bits es apropiado). En la mayoría de los casos, tiene sentido jugar con los pedidos de estos campos para lograr el mejor empaque y/o rendimiento.


También hay un uso secundario raro de campos de bits: usarlos para grupos de asignación de bits en diversas representaciones externa especificados, como los registros de hardware, formatos de punto flotante, formatos de archivo, etc. Esto nunca se ha concebido como un uso adecuado de los campos de bits, aunque por alguna razón no explicada, este tipo de abuso de campo de bits sigue apareciendo en el código de la vida real. Simplemente no hagas esto.

+0

En mi código, uso macros de bit shift para empacar campos pequeños como ese. Esto evita los aspectos de campos de bit definidos por la implementación. (No estoy seguro de si OP preguntaba específicamente sobre el uso de la sintaxis del campo de bits de C, o sobre el concepto de empaquetar campos menores que un byte en general) –