¿Hay alguna ventaja en la manipulación de bits de estilo c sobre std :: bitset?

16

Trabajo casi exclusivamente en C ++ 11/14, y generalmente me estremezco cuando veo un código como este:

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

Esto es solo un ejemplo; Estoy hablando de manipulación de bits en general. En C ++, ¿hay realmente algún punto? Lo anterior es alucinante y propenso a errores, mientras que el uso de le std::bitsetpermite:

  1. modifique más fácilmente el tamaño de la std::bitsetque sea necesario ajustando un parámetro de plantilla y permitiendo que la implementación se encargue del resto, y
  2. Dedique menos tiempo a descubrir qué está sucediendo (y posiblemente cometer errores) y escriba std::bitsetde manera similar a std::arrayotros contenedores de datos.

Mi pregunta es; ¿Hay alguna razón para no usar std::bitsetsobre tipos primitivos, aparte de la compatibilidad con versiones anteriores?

cuant
fuente
El tamaño de a std::bitsetse fija en tiempo de compilación. Ese es el único inconveniente paralizante que se me ocurre.
rwong
1
@rwong estoy hablando de la std::bitsetmanipulación de bits frente al estilo c (por ejemplo int), que también se corrige en tiempo de compilación.
Quant
Una razón podría ser el código heredado: el código se escribió cuando std::bitsetno estaba disponible (o el autor no lo conocía) y no ha habido una razón para reescribir el código para usarlo std::bitset.
Bart van Ingen Schenau
Personalmente, creo que la cuestión de cómo hacer que "las operaciones en un conjunto / mapa / matriz de variables binarias" sea fácil de entender para todos todavía está en gran parte sin resolver, porque hay muchas operaciones utilizadas en la práctica que no se pueden reducir a operaciones simples. También hay muchas maneras de representar tales conjuntos, de los cuales bitsetuno es, pero un pequeño vector o conjunto de ints (de índice de bits) también podría ser legítimo. La filosofía de C / C ++ no oculta estas complejidades de elección del programador.
rwong

Respuestas:

12

Desde un punto de vista lógico (no técnico), no hay ventaja.

Cualquier código C / C ++ simple se puede incluir dentro de una "construcción de biblioteca" adecuada. Después de tal ajuste, la cuestión de "si esto es más ventajoso que eso" se convierte en una cuestión discutible.

Desde el punto de vista de la velocidad, C / C ++ debería permitir que la construcción de la biblioteca genere código que sea tan eficiente como el código simple que envuelve. Sin embargo, esto está sujeto a:

  • Función en línea
  • Comprobación en tiempo de compilación y eliminación de comprobaciones de tiempo de ejecución innecesarias
  • Eliminación de código muerto
  • Muchas otras optimizaciones de código ...

Usando este tipo de argumento no técnico, cualquier "función faltante" podría ser agregada por cualquier persona y, por lo tanto, no se considera una desventaja.

Sin embargo, los requisitos y limitaciones incorporados no se pueden superar con código adicional. A continuación, sostengo que el tamaño de std::bitsetes una constante de tiempo de compilación y, por lo tanto, si bien no se considera una desventaja, sigue siendo algo que afecta la elección del usuario.


Desde un punto de vista estético (legibilidad, facilidad de mantenimiento, etc.), hay una diferencia.

Sin embargo, no es aparente que el std::bitsetcódigo inmediatamente gane sobre el código C simple. Uno tiene que mirar piezas de código más grandes (y no una muestra de juguete) para decir si el uso de std::bitsetha mejorado la calidad humana del código fuente.


La velocidad de la manipulación de bits depende del estilo de codificación. El estilo de codificación afecta tanto a la manipulación de bits C / C ++, y también es igualmente aplicable std::bitset, como se explica a continuación.


Si uno escribe código que usa el operator []para leer y escribir un bit a la vez, tendrá que hacerlo varias veces si hay más de un bit para manipular. Lo mismo puede decirse del código de estilo C.

Sin embargo, bitsettambién tiene otros operadores, como operator &=, operator <<=etc., que opera en todo el ancho del conjunto de bits. Debido a que la maquinaria subyacente a menudo puede operar en 32 bits, 64 bits y, a veces, 128 bits (con SIMD) a la vez (en el mismo número de ciclos de CPU), código diseñado para aprovechar tales operaciones de múltiples bits puede ser más rápido que el código de manipulación de bits "en bucle".

La idea general se llama SWAR (SIMD dentro de un registro) , y es un subtema bajo manipulaciones de bits.


Algunos proveedores de C ++ pueden implementar bitsetentre 64 bits y 128 bits con SIMD. Es posible que algunos proveedores no lo hagan (pero eventualmente lo harán). Si es necesario saber qué está haciendo la biblioteca del proveedor de C ++, la única forma es mirar el desmontaje.


En cuanto a si std::bitsettiene limitaciones, puedo dar dos ejemplos.

  1. El tamaño de std::bitsetdebe ser conocido en tiempo de compilación. Para hacer una matriz de bits con un tamaño elegido dinámicamente, uno tendrá que usar std::vector<bool>.
  2. La especificación actual de C ++ para std::bitsetno proporciona una forma de extraer una porción consecutiva de N bits de una bitsetM mayor .

El primero es fundamental, lo que significa que para las personas que necesitan conjuntos de bits de tamaño dinámico, deben elegir otras opciones.

El segundo se puede superar, porque uno puede escribir algún tipo de adaptadores para realizar la tarea, incluso si el estándar bitsetno es extensible.


Existen ciertos tipos de operaciones SWAR avanzadas que no se proporcionan desde el primer momento std::bitset. Uno podría leer sobre estas operaciones en este sitio web sobre permutaciones de bits . Como de costumbre, uno puede implementarlos por su cuenta, operando además std::bitset.


En cuanto a la discusión sobre el rendimiento.

Una advertencia: mucha gente pregunta por qué (algo) de la biblioteca estándar es mucho más lento que un código simple de estilo C. No repetiría los requisitos previos de microbenchmarking aquí, pero solo tengo este consejo: asegúrese de comparar en el "modo de lanzamiento" (con optimizaciones habilitadas), y asegúrese de que el código no se elimine (eliminación de código muerto) o sea izada fuera de un bucle (movimiento de código invariante de bucle) .

Dado que, en general, no podemos decir si alguien (en Internet) estaba haciendo los microbenchmarks correctamente, la única forma en que podemos llegar a una conclusión confiable es hacer nuestros propios microbenchmarks, documentar los detalles y someterlos a revisión y crítica pública. No está de más rehacer microbenchmarks que otros han hecho antes.

rwong
fuente
El problema n. ° 2 también significa que el conjunto de bits no se puede utilizar en ninguna configuración paralela en la que cada subproceso funcione en un subconjunto del conjunto de bits.
user239558
@ user239558 Dudo que alguien quiera paralelizar en lo mismo std::bitset. No hay garantía de coherencia de memoria (in std::bitset), lo que significa que no se debe compartir entre núcleos. Las personas que necesitan compartirlo entre núcleos tenderán a construir su propia implementación. Cuando los datos se comparten entre diferentes núcleos, es habitual alinearlos con el límite de la línea de caché. No hacerlo reduce el rendimiento y expone más dificultades de no atomicidad. No tengo suficiente conocimiento para dar una visión general sobre cómo construir una implementación paralelizable de std::bitset.
rwong
La programación paralela de datos generalmente no requiere ninguna coherencia de memoria. solo sincronizas entre fases. Absolutamente me gustaría procesar un bitset en paralelo, creo que cualquiera con una gran bitsetvoluntad.
user239558
@ user239558 que suena como si implicara copiar (el rango relevante de conjunto de bits que procesará cada núcleo debe copiarse antes de que comience el procesamiento). Estoy de acuerdo con eso, aunque creo que cualquiera que esté pensando en la paralelización ya pensaría en implementar su propia implementación. En general, se proporcionan muchas instalaciones de biblioteca estándar de C ++ como implementaciones de línea de base; cualquiera que tenga necesidades más serias implementará las suyas.
rwong
No, no hay copia. es simplemente acceder a diferentes partes de una estructura de datos estática. entonces no se necesita sincronización.
user239558
2

Ciertamente, esto no se aplica en todos los casos, pero ocasionalmente un algoritmo puede depender de la eficiencia del giro de bits de estilo C para proporcionar ganancias de rendimiento significativas. El primer ejemplo que me viene a la mente es el uso de bitboards , codificaciones enteras inteligentes de las posiciones de los juegos de mesa, para acelerar los motores de ajedrez y similares. Aquí, el tamaño fijo de los tipos enteros no es un problema, ya que los tableros de ajedrez siempre son 8 * 8 de todos modos.

Para un ejemplo simple, considere la siguiente función (tomada de esta respuesta por Ben Jackson ) que prueba una posición de Connect Four para la victoria:

// 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
fuente
2
¿Crees que un std::bitsetsería más lento?
Quant
Bueno, de un vistazo rápido a la fuente, el conjunto de bits libc ++ se basa en un único size_t o una matriz de ellos, por lo que probablemente se compile en algo esencialmente equivalente / idéntico, especialmente en un sistema donde sizeof (size_t) == 8 - así que no, probablemente no sería más lento.
Ryan Pavlik