c++ - ¿Cuándo vale la pena usar los campos de bits?
bit-fields (11)
Boost.Thread utiliza bitfields 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;
};
¿Vale la pena utilizar la implementación del campo de bits de C? Si es así, ¿cuándo se usa?
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)?
¿Todavía hay momentos en los que se usan campos de bits? (es decir, firmware para colocar fichas reales, etc.)
El objetivo principal de los campos de bits es proporcionar una forma de ahorrar memoria en estructuras de datos agregados masivamente mediante el logro de un ajuste más estricto de los datos.
La idea general 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, digamos, 32 de ellos que pueden empaquetarse en una única unidad de asignación unsigned int
).
Obviamente, esto solo tiene sentido en una situación en la que las ventajas del consumo reducido de memoria superan los inconvenientes de un acceso más lento a los valores almacenados en los 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 struct 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.
Entonces, eso nos da un conjunto bastante claro de condiciones bajo las cuales vale la pena emplear campos de bits:
- El tipo de estructura contiene múltiples campos que se pueden empaquetar en un número menor de bits.
- El programa crea una gran cantidad de objetos de ese tipo de estructura.
Si se cumplen las condiciones, declara todos los campos bit-packable de forma contigua (generalmente al final del tipo de estructura), asígneles sus anchos de bits adecuados (y, por lo general, tome algunas medidas para asegurarse de que los bits sean adecuados) . 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 extraño uso secundario de los campos de bits: usarlos para mapear grupos de bits en varias representaciones especificadas externamente, como registros de hardware, formatos de punto flotante, formatos de archivo, etc. Esto nunca ha sido concebido como un uso adecuado de los campos de bits , aunque por algún motivo inexplicable, este tipo de abuso de campo de bits sigue apareciendo en el código de la vida real. Simplemente no hagas esto.
En el código moderno, solo hay una razón para usar bitfields: para controlar los requisitos de espacio de un bool
o un tipo 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 */
};
IMO básicamente no hay razón para no usar :1
bitfields para bool
, ya que los compiladores modernos generarán un código muy eficiente para él. Sin embargo, en C, asegúrese de que bool
typedef sea C99 _Bool
o que no sea un int sin firmar , porque un campo de 1 bit firmado solo puede contener los valores 0
y -1
(a menos que de alguna manera tenga una máquina que no sea de dos en dos )
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 (ciclos repetidos de lectura, modificación y escritura, 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 le da suficiente control sobre el endianness , padding y (para I / O regs) exactamente qué secuencias de ensamblaje 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.
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.
- Una pantalla de 32 columnas poco controladas frente a 64 columnas.
- El bit 0 en esa misma celda de memoria era la entrada / salida de datos en serie del casete.
Según 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.
FWIW, y mirando solo a la pregunta de 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 en ~ 20% de ejecución para correr):
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 una sobrecarga del ciclo, 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: el usuario2188211 intentó una edición que fue rechazada pero ilustró cómo los campos de bits se vuelven más rápidos a medida que aumenta la cantidad de datos: "al iterar sobre un vector de unos pocos millones de elementos en [una versión modificada] del código anterior, tal que las variables no reside en la caché o los 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 de un ejemplo ejecutado (g ++ -03, Core2Duo):
0.19016
bitfields: 1449991808
0.342756
separate ints: 1449991808
0.215243
explicit and/or/shift: 1449991808
Por supuesto, el tiempo es relativo y la forma en que implemente estos campos puede no importar en absoluto en el contexto de su sistema.
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 usaban campos de bits con frecuencia para aprovechar al máximo la memoria de la computadora / disco. Por ejemplo, para una definición de NPC en un RPG, puede encontrar (ejemplo compuesto):
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...
};
No se ve tanto en los juegos / software más modernos ya que el ahorro de espacio se ha empeorado proporcionalmente a medida que las computadoras obtienen más memoria. Guardar un 1MB de memoria cuando su computadora solo tiene 16MB es un gran problema, pero no tanto cuando tiene 4GB.
Los campos de bits se usaron en los viejos tiempos 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 todo el tiempo para usar realmente los valores).
¡Consulte la fuente de http://www.nethack.org/ para ver pre ansi c en toda su gloria bitfield!
Normalmente, los campos de bits solo se utilizan cuando existe la necesidad de asignar los campos de la estructura a segmentos de bits específicos, donde algún hardware interpretará 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!
Si bien los campos de bits pueden conducir a una sintaxis ordenada, 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 usa 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.
Un uso para los campos de bits que aún no se ha mencionado es que los unsigned
bits unsigned
proporcionan un módulo aritmético una potencia de dos "gratis". Por ejemplo, dado:
struct { unsigned x:10; } foo;
la aritmética en foo.x
se realizará modulo 2 10 = 1024.
(Lo mismo se puede lograr directamente mediante el uso de operaciones a nivel de bit, por supuesto, pero a veces puede conducir a un código más claro para que el compilador lo haga por usted).
Una alternativa a considerar es especificar estructuras de campo de bit con una estructura ficticia (nunca instanciada) donde cada byte representa un bit:
struct Bf_format
{
char field1[5];
char field2[9];
char field3[18];
};
Con este tamaño de aproximación se obtiene el ancho del campo de bit, y offsetof da 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 definir y usar estructuras de campos de bits de una manera mucho más flexible, mucho más flexible y funcional: 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.
Uno de los usos de los campos de bit solía ser el de los 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.