Pregunta ¿Por qué uint32_t sería preferible en lugar de uint_fast32_t?


Parece que uint32_t es mucho más frecuente que uint_fast32_t (Me doy cuenta de que esto es evidencia anecdótica). Sin embargo, eso parece contrario a la intuición para mí.

Casi siempre cuando veo una implementación uso uint32_t, todo lo que realmente quiere es un número entero que puede contener valores de hasta 4,294,967,295 (generalmente un límite mucho más bajo en algún lugar entre 65,535 y 4,294,967,295).

Parece extraño usarlo uint32_t, como el 'exactamente 32 bits' la garantía no es necesaria, y el 'más rápido disponible> = 32 bits' garantía de uint_fast32_t parece ser exactamente la idea correcta. Además, mientras que generalmente se implementa, uint32_t no está garantizado que exista.

Por qué, entonces, lo haría uint32_t ser preferido? ¿Es simplemente mejor conocido o hay ventajas técnicas sobre el otro?


74
2017-10-26 16:46


origen


Respuestas:


uint32_t se garantiza que tiene casi las mismas propiedades en cualquier plataforma que lo soporte.1

uint_fast32_t tiene muy pocas garantías sobre cómo se comporta en diferentes sistemas en comparación.

Si cambias a una plataforma donde uint_fast32_t tiene un tamaño diferente, todo el código que usa uint_fast32_t tiene que ser probado nuevamente y validado. Todos los supuestos de estabilidad van a estar fuera de la ventana. Todo el sistema va a funcionar de manera diferente.

Al escribir su código, es posible que ni siquiera tenga acceso a un uint_fast32_t sistema que no tiene 32 bits de tamaño.

uint32_t no funcionará de manera diferente (excepto en la nota a pie de página).

La corrección es más importante que la velocidad. La corrección prematura es, por lo tanto, un mejor plan que la optimización prematura.

En el caso de que estaba escribiendo código para sistemas donde uint_fast32_t tenía 64 o más bits, podría probar mi código para ambos casos y usarlo. Salvo la necesidad y la oportunidad, hacerlo es un mal plan.

Finalmente, uint_fast32_t cuando lo está almacenando por un período de tiempo o el número de instancias puede ser más lento que uint32 simplemente debido a problemas de tamaño de caché y ancho de banda de memoria. Las computadoras de hoy en día están mucho más atadas a la memoria que la CPU, y uint_fast32_t podría ser más rápido de forma aislada, pero no después de que tenga en cuenta la sobrecarga de memoria.


1 Como @chux ha notado en un comentario, si unsigned Es mas grande que uint32_t, aritmética en uint32_t pasa por las promociones enteras habituales, y si no, se mantiene como uint32_t. Esto puede causar errores. Nada es perfecto.


73
2017-10-26 17:34



¿Por qué muchas personas usan uint32_t más bien que uint32_fast_t?

Nota: mal llamado uint32_fast_t debiera ser uint_fast32_t.

uint32_t tiene una especificación más estricta que uint_fast32_t y así lo hace para una funcionalidad más consistente.


uint32_t pros:

  • Varios algoritmos especifican este tipo. IMO: la mejor razón para usar.
  • Ancho y rango exactos conocidos.
  • Las matrices de este tipo no incurren en desperdicio.
  • no firmado las matemáticas enteras con su desbordamiento son más predecibles.
  • Concordancia más cercana en el rango y matemática de los tipos de 32 bits de otros idiomas.
  • Nunca acolchado.

uint32_t contras:

  • No siempre disponible (aún esto es raro en 2018).
    Por ejemplo: Plataformas que carecen de enteros de 8/16/32 bits (9/18 /36-poco, otros)
    Por ejemplo: Plataformas que usan complemento de no 2. viejo 2200

uint_fast32_t pros:

  • Siempre disponible.
    Esta siempre permitir que todas las plataformas, nuevas y viejas, usen tipos rápidos / mínimos.
  • "Lo más rápido" tipo que admite rango de 32 bits.

uint_fast32_t contras:

  • El rango es solo mínimamente conocido. Ejemplo, podría ser un tipo de 64 bits.
  • Las matrices de este tipo pueden desperdiciar memoria.
  • Todas las respuestas (la mía también al principio), la publicación y los comentarios usaron el nombre incorrecto uint32_fast_t. Parece que muchos simplemente no necesitan y usan este tipo. ¡Ni siquiera usamos el nombre correcto!
  • Relleno posible - (raro).
  • En casos seleccionados, el tipo "más rápido" puede ser realmente otro tipo. Asi que uint_fast32_t es solo una aproximación de primer orden.

Al final, lo mejor depende del objetivo de codificación. A menos que esté codificado para una portabilidad muy amplia o alguna función de rendimiento de nicho, use uint32_t.


Hay otro problema al usar estos tipos que entra en juego: su rango en comparación con int/unsigned

Presumiblemente uint_fastN_t sería al menos el rango de unsigned. Esto no está especificado, pero es una condición cierta y comprobable.

Así, uintN_t es más probable que uint_fastN_t ser más estrecho unsigned. Esto significa que el código que usa uintN_t las matemáticas están más sujetas a promociones enteras que uint_fastN_t cuando se trata de la portabilidad.

Con esta preocupación: ventaja de portabilidad uint_fastN_t con operaciones matemáticas seleccionadas


Nota al pie sobre int32_t más bien que int_fast32_t: En máquinas raras, INT_FAST32_MIN puede ser -2,147,483,647 y no -2,147,483,648. El punto más grande: (u)intN_t tipos están estrechamente especificados y conducen a código portátil.


29
2017-10-26 17:32



¿Por qué muchas personas usan uint32_t más bien que uint32_fast_t?

Tonta respuesta:

  • No hay un tipo estándar uint32_fast_t, la ortografía correcta es uint_fast32_t.

Respuesta práctica:

  • Mucha gente realmente usa uint32_t o int32_t por su semántica precisa, exactamente 32 bits con aritmética sin signo envolvente (uint32_t) o la representación complementaria de 2 (int32_t) los xxx_fast32_t los tipos pueden ser más grandes y, por lo tanto, inapropiados para almacenar en archivos binarios, usarlos en matrices y estructuras empaquetadas o enviarlos a través de una red. Además, es posible que ni siquiera sean más rápidos.

Respuesta pragmática:

  • Muchas personas simplemente no saben (o simplemente no les importa) acerca de uint_fast32_t, como se demuestra en los comentarios y respuestas, y probablemente asuma unsigned int tener la misma semántica, aunque muchas arquitecturas actuales todavía tienen 16 bits inty algunas muestras raras del Museo tienen otros tamaños int extraños menos de 32.

Respuesta UX:

  • Aunque posiblemente sea más rápido que uint32_t, uint_fast32_t es más lento de usar: lleva más tiempo escribir, especialmente para buscar la ortografía y la semántica en la documentación de C ;-)

La elegancia importa, (obviamente basada en la opinión):

  • uint32_t parece tan malo que muchos programadores prefieren definir su propio u32 o uint32 tipo ... Desde esta perspectiva, uint_fast32_t se ve torpe más allá de la reparación. No es de extrañar que se siente en el banco con sus amigos uint_least32_ty tal.

24
2017-10-26 18:50



Una razón es que unsigned int ya es "más rápido" sin la necesidad de ningún typedefs especial o la necesidad de incluir algo. Entonces, si lo necesita rápido, solo use el int o unsigned int tipo.
Si bien el estándar no garantiza explícitamente que sea más rápido, indirectamente lo hace al indicar "Los colores normales tienen el tamaño natural sugerido por la arquitectura del entorno de ejecución" en 3.9.1. En otras palabras, int (o su contraparte sin signo) es con lo que el procesador se siente más cómodo.

Ahora, por supuesto, no sabes qué tamaño unsigned int puede ser. Solo sabes que es al menos tan grande como short (y parece recordar eso short debe ser de al menos 16 bits, ¡aunque ahora no puedo encontrarlo en el estándar!). Por lo general, es simplemente 4 bytes, pero en teoría podría ser más grande o, en casos extremos, incluso más pequeño (aunque personalmente nunca me encontré con una arquitectura donde este era el caso, ni siquiera en computadoras de 8 bits en la década de 1980 ... tal vez algunos microcontroladores, quién sabe resulta que sufro de demencia, int era claramente 16 bits en aquel entonces).

El estándar C ++ no se molesta en especificar qué <cstdint> Los tipos son o lo que garantizan, simplemente menciona "lo mismo que en C".

uint32_t, según el estándar C, garantías que obtienes exactamente 32 bits No hay nada diferente, nada menos y nada de relleno. A veces esto es exactamente lo que necesita, y por lo tanto es muy valioso.

uint_least32_t garantiza que sea cual sea el tamaño, no puede ser más pequeño que 32 bits (pero podría ser mucho más grande). A veces, pero mucho más raramente que un witdh exacto o "no me importa", esto es lo que quieres.

Finalmente, uint_fast32_t es un tanto superfluo en mi opinión, excepto por los propósitos de la documentación de intención. Los estados estándar C "designa un tipo de entero que generalmente es el más rápido" (tenga en cuenta la palabra "generalmente") y explícitamente menciona que no necesita ser más rápido para todos los propósitos. En otras palabras, uint_fast32_t es casi lo mismo que uint_least32_t, cual es generalmente más rápido también, solo no se da ninguna garantía (pero no hay garantía de ninguna manera).

Como la mayoría de las veces no le importa el tamaño exacto o desea exactamente 32 (o 64, a veces 16) bits, y desde el "no me importa" unsigned int el tipo es el más rápido de todos modos, esto explica por qué uint_fast32_t no se usa tan frecuentemente


8
2017-10-26 20:01



No he visto evidencia de que uint32_t ser usado para sus distancia. En cambio, la mayor parte del tiempo que He visto uint32_t se usa, es para contener exactamente 4 octetos de datos en varios algoritmos, ¡con semáforos envolventes y de cambio garantizados!

También hay otras razones para usar uint32_t en lugar de uint_fast32_t: A menudo es que proporcionará ABI estable. Además, el uso de memoria se puede conocer con precisión. Esto compensa en gran medida cualquier ganancia de velocidad de uint_fast32_t, cuando ese tipo sería distinto del de uint32_t.

Para valores <65536, ya existe un tipo práctico, se llama unsigned int (unsigned short se requiere que tenga al menos ese rango también, pero unsigned int es del tamaño de palabra nativo) Para valores <4294967296, hay otro llamado unsigned long.


Y, por último, la gente no usa uint_fast32_t porque es molestamente largo de escribir y fácil de escribir mal: D


6
2017-10-26 17:23



Muchas rasones.

  1. Muchas personas no saben que existen los tipos 'rápidos'.
  2. Es más detallado para escribir.
  3. Es más difícil razonar sobre el comportamiento de su programa cuando no conoce el tamaño real del tipo.
  4. El estándar en realidad no identifica al más rápido, ni puede realmente qué tipo es realmente más rápido puede depender mucho del contexto.
  5. No he visto evidencia de que los desarrolladores de plataformas pongan algún pensamiento en el tamaño de estos tipos al definir sus plataformas. Por ejemplo, en x86-64 Linux, los tipos "rápidos" son todos de 64 bits, aunque x86-64 tiene soporte de hardware para operaciones rápidas en valores de 32 bits.

En resumen, los tipos "rápidos" son basura sin valor. Si realmente necesita averiguar qué tipo es el más rápido para una aplicación determinada, necesita comparar su código en su compilador.


5
2017-10-26 22:25



Desde el punto de vista de la corrección y la facilidad de codificación, uint32_t tiene muchas ventajas sobre uint_fast32_t en particular, debido a la semántica de tamaño y aritmética definida más precisamente, como muchos usuarios han señalado anteriormente.

Lo que quizás se haya perdido es que una supuesta ventaja de uint_fast32_t - que puede ser Más rápido, simplemente nunca se materializó de ninguna manera significativa. La mayoría de los procesadores de 64 bits que han dominado la era de 64 bits (x86-64 y Aarch64 en su mayoría) evolucionaron a partir de arquitecturas de 32 bits y tienen rápido Operaciones nativas de 32 bits incluso en modo de 64 bits. Asi que uint_fast32_t es lo mismo que uint32_t en esas plataformas.

Incluso si algunas de las plataformas "también ejecutadas" como POWER, MIPS64, SPARC solo ofrecen operaciones de ALU de 64 bits, el amplia mayoría de interesantes operaciones de 32 bits se puede hacer muy bien en los registros de 64 bits: la parte inferior de 32 bits tendrá los resultados deseados (y todas las plataformas convencionales al menos le permiten cargar / almacenar 32 bits). El desplazamiento a la izquierda es la principal problemática, pero incluso eso se puede optimizar en muchos casos mediante optimizaciones de seguimiento del valor / rango en el compilador.

Dudo que ocasionalmente el cambio a la izquierda ligeramente más lento o la multiplicación de 32x32 -> 64 supere doble el uso de la memoria para tales valores, en todas las aplicaciones menos en las más oscuras.

Finalmente, señalaré que, si bien la compensación se ha caracterizado en gran medida como "uso de memoria y potencial de vectorización" (a favor de uint32_t) versus el conteo / velocidad de instrucción (a favor de uint_fast32_t) - incluso eso no está claro para mí. Sí, en algunas plataformas necesitarás instrucciones adicionales para algunos Operaciones de 32 bits, pero también salvar algunas instrucciones porque:

  • Usar un tipo más pequeño a menudo permite que el compilador combine inteligentemente las operaciones adyacentes al usar una operación de 64 bits para lograr dos de 32 bits. Un ejemplo de este tipo de "vectorización del hombre pobre" no es infrecuente. Por ejemplo, crear una constante struct two32{ uint32_t a, b; } dentro rax me gusta two32{1, 2}  puede ser optimizado en un solo mov rax, 0x20001 mientras que la versión de 64 bits necesita dos instrucciones. En principio, esto también debería ser posible para operaciones aritméticas adyacentes (la misma operación, diferentes operandos), pero no lo he visto en la práctica.
  • Un menor "uso de memoria" también conduce a menudo a menos instrucciones, incluso si la memoria o el espacio en caché no es un problema, ya que cualquier estructura de tipo o matrices de este tipo se copian, se obtiene el doble de su dinero por registro copiado.
  • Los tipos de datos más pequeños a menudo explotan mejores convenciones de llamadas modernas como SysV ABI, que empaqueta los datos de la estructura de datos de manera eficiente en los registros. Por ejemplo, puede devolver hasta una estructura de 16 bytes en los registros rdx:rax. Para una estructura de retorno de funciones con 4 uint32_t valores (inicializados a partir de una constante), que se traduce en

    ret_constant32():
        movabs  rax, 8589934593
        movabs  rdx, 17179869187
        ret
    

    La misma estructura con 4 de 64 bits uint_fast32_t necesita un movimiento de registro y cuatro tiendas a la memoria para hacer lo mismo (y la persona que llama probablemente tendrá que volver a leer los valores de la memoria después de la devolución):

    ret_constant64():
        mov     rax, rdi
        mov     QWORD PTR [rdi], 1
        mov     QWORD PTR [rdi+8], 2
        mov     QWORD PTR [rdi+16], 3
        mov     QWORD PTR [rdi+24], 4
        ret
    

    De manera similar, al pasar argumentos de estructura, los valores de 32 bits se empaquetan casi dos veces más densamente en los registros disponibles para los parámetros, por lo que es menos probable que se agoten los argumentos de registro y tengan que pasar a la pila1.

  • Incluso si elige usar uint_fast32_t para los lugares donde "la velocidad importa" a menudo también tendrá lugares donde necesita un tipo de tamaño fijo. Por ejemplo, al pasar valores de salida externa, de entrada externa, como parte de su ABI, como parte de una estructura que necesita un diseño específico, o porque usted usa inteligentemente uint32_t para grandes agregaciones de valores para ahorrar en la huella de memoria. En los lugares donde tu uint_fast32_t y los tipos `` uint32_t` necesitan interfaz, puede encontrar (además de la complejidad del desarrollo), extensiones innecesarias de signos u otro código relacionado con desajuste de tamaño. Los compiladores hacen un buen trabajo al optimizar esto en muchos casos, pero no es inusual ver esto en una salida optimizada cuando se mezclan tipos de diferentes tamaños.

Puedes jugar con algunos de los ejemplos anteriores y más en godbolt.


1 Para ser claros, la convención de las estructuras de empaquetado en los registros no siempre es una clara victoria para los valores más pequeños. Significa que los valores más pequeños pueden tener que "extraerse" antes de que puedan ser utilizados. Por ejemplo, una función simple que devuelve la suma de los dos miembros de la estructura juntos necesita una mov rax, rdi; shr rax, 32; add edi, eax mientras que para la versión de 64 bits, cada argumento tiene su propio registro y solo necesita un solo add o lea. Aún así, si acepta que el diseño de "paquete apretado de estructuras al pasar" tiene sentido en general, entonces los valores más pequeños aprovecharán más esta característica.


5
2017-10-28 05:09