¿Por qué se preferiría uint32_t en lugar de uint_fast32_t?

81

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

Casi siempre, cuando veo el uso de una implementación uint32_t, todo lo que realmente quiere es un número entero que pueda 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 luego uint32_t, ya que la garantía de 'exactamente 32 bits' no es necesaria, y la garantía de 'más rápido disponible> = 32 bits'uint_fast32_t parece ser exactamente la idea correcta. Además, aunque generalmente se implementa, en uint32_trealidad no se garantiza que exista.

Entonces, ¿por qué uint32_tse preferiría? ¿Es simplemente más conocido o existen ventajas técnicas sobre el otro?

Joost
fuente
24
Respuesta simple, ¿tal vez necesitan un número entero que tenga exactamente 32 bits?
Stargateur
7
Lo primero que escuché uint32_fast_t, que si lo entiendo correctamente, es de al menos 32 bits (¿significa que podría ser más? Me parece engañoso). Actualmente estoy usando uint32_ty amigos en mi proyecto porque estoy empaquetando estos datos y enviándolos a través de la red, y quiero que el remitente y el receptor sepan exactamente qué tan grandes son los campos. Parece que esta puede no ser la solución más robusta ya que una plataforma puede no implementarse uint32_t, pero todas las mías aparentemente lo hacen, así que estoy bien con lo que estoy haciendo.
yano
5
@yano: Para las redes, también debería preocuparse por el orden de bytes / endianess, eso uint32_tno le da eso (y es una lástima que no haya uint32_t_bey uint32_t_le, que sería más apropiado para casi todos los casos posibles en los uint32_tque actualmente es la mejor opción).
Brendan
3
@Brendan: con respecto a _be y _le, ¿htonl () y ntohl () proporcionarían la misma capacidad?
mpez0
2
@Brendan es un objeto bastante pesado para ocultar en un int estándar, todos los cuales son tipos primitivos. Estoy de acuerdo con usted en principio en que esto debería manejarse en alguna parte del estándar, pero creo que este podría no ser el lugar
Steve Cox

Respuestas:

78

uint32_tse garantiza que tendrá casi las mismas propiedades en cualquier plataforma que lo admita. 1

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

Si cambia a una plataforma que uint_fast32_ttiene un tamaño diferente, todo el código que se utiliza uint_fast32_tdebe volver a probarse y validarse. Todos los supuestos de estabilidad se perderán. Todo el sistema funcionará de manera diferente.

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

uint32_t no funcionará de manera diferente (ver nota al pie).

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

En el caso de que estuviera escribiendo código para sistemas con uint_fast32_t64 bits o más, podría probar mi código para ambos casos y usarlo. Salvo necesidad y oportunidad, hacerlo es un mal plan.

Finalmente, uint_fast32_tcuando lo está almacenando durante cualquier período de tiempo o número de instancias, puede ser más lento que uint32simplemente debido a problemas de tamaño de caché y ancho de banda de memoria. Las computadoras de hoy están mucho más ligadas a la memoria que a la CPU, y uint_fast32_tpodrían ser más rápidas de forma aislada, pero no después de tener en cuenta la sobrecarga de la memoria.


1 Como @chux ha señalado en un comentario, si unsignedes mayor que uint32_t, la aritmética activa uint32_tpasa por las promociones habituales de enteros, y si no, permanece como uint32_t. Esto puede provocar errores. Nada es perfecto.

Yakk - Adam Nevraumont
fuente
15
"Se garantiza que uint32_t tendrá las mismas propiedades en cualquier plataforma que lo admita". Hay un problema de esquina cuando unsignedes más ancho que uint32_ty luego uint32_ten una plataforma pasa por las promociones enteras habituales y en otra no. Sin embargo, con uint32_testos problemas matemáticos enteros se reducen significativamente.
chux - Reincorporación a Monica
2
@chux es un caso de esquina que puede causar UB al multiplicar, porque la promoción prefiere int con signo y el desbordamiento de enteros con signo es UB.
CodesInChaos
2
Aunque esta respuesta es correcta en la medida de lo posible, minimiza en gran medida los detalles clave. En pocas palabras, uint32_tes para donde los detalles exactos de la representación de la máquina del tipo son importantes, mientras que uint_fast32_tes para donde la velocidad computacional es más importante, el (des) signo y el rango mínimo son importantes, y los detalles de representación no son esenciales. También hay uint_least32_tdonde la (des) firma y el rango mínimo son más importantes, la compacidad es más importante que la velocidad, y la representación exacta no es esencial.
John Bollinger
@JohnBollinger Lo cual está muy bien, pero sin probar el hardware real que implementa más de una variante, los tipos de tamaño variable son una trampa. Y la razón por la que la gente usa en uint32_tlugar de los otros tipos es porque generalmente no tienen ese hardware para realizar pruebas . (Lo mismo ocurre int32_ten menor medida, e incluso inty short).
Yakk - Adam Nevraumont
1
Un ejemplo del caso de la esquina: Let unsigned short== uint32_ty int== int48_t. Si calcula algo como (uint32_t)0xFFFFFFFF * (uint32_t)0xFFFFFFFF, entonces los operandos se promocionan signed inty desencadenarán un desbordamiento de entero con signo, que es un comportamiento indefinido. Vea esta pregunta.
Nayuki
32

¿Por qué mucha gente usa en uint32_tlugar de uint32_fast_t?

Nota: El nombre incorrecto uint32_fast_tdebería ser uint_fast32_t.

uint32_ttiene una especificación más estricta uint_fast32_ty, por lo tanto, ofrece una funcionalidad más consistente.


uint32_t pros:

  • Varios algoritmos especifican este tipo. En mi opinión, la mejor razón para usar.
  • Ancho y rango exactos conocidos.
  • Las matrices de este tipo no generan desperdicio.
  • La matemática de enteros sin signo con su desbordamiento es más predecible.
  • Coincidencia más cercana en rango y matemáticas de los tipos de 32 bits de otros idiomas.
  • Nunca acolchado.

uint32_t contras:

  • No siempre está disponible (sin embargo, esto es raro en 2018).
    Ej: Plataformas que carecen de 8/16 números enteros / 32 bits (9/18/ 36 -bit, otros ).
    Ej .: Plataformas que utilizan un complemento distinto de 2. viejo 2200

uint_fast32_t pros:

  • Siempre disponible.
    Esto siempre permite que todas las plataformas, nuevas y antiguas, utilicen tipos rápidos / mínimos.
  • Tipo "más rápido" que admite el rango de 32 bits.

uint_fast32_t contras:

  • El alcance se conoce sólo mínimamente. Por 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 ni usan este tipo. ¡Ni siquiera usamos el nombre correcto!
  • Acolchado posible - (raro).
  • En casos seleccionados, el tipo "más rápido" puede ser realmente otro tipo. Entonces uint_fast32_tes solo una aproximación de primer orden.

Al final, lo que es mejor depende del objetivo de codificación. A menos que se codifique para una portabilidad muy amplia o alguna función de rendimiento mejorada, utilice uint32_t.


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

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

Por lo tanto, uintN_tes más probable que uint_fastN_tsea ​​más estrecho el unsigned. Esto significa que uintN_tes más probable que el código que usa matemáticas esté sujeto a promociones de números enteros que uint_fastN_tcuando se trata de portabilidad.

Con esta preocupación: ventaja de portabilidad uint_fastN_tcon operaciones matemáticas seleccionadas.


Nota al margen sobre en int32_tlugar de int_fast32_t: en máquinas raras, INT_FAST32_MINpuede ser -2,147,483,647 y no -2,147,483,648. El punto más importante: los (u)intN_ttipos están estrictamente especificados y conducen a un código portátil.

chux - Restablecer a Monica
fuente
2
El tipo más rápido que admite el rango de 32 bits => ¿de verdad? Esta es una reliquia de una época en la que la RAM funcionaba a velocidades de CPU, hoy en día el equilibrio ha cambiado drásticamente en las PC, por lo que (1) extraer enteros de 32 bits de la memoria es dos veces más rápido que extraer los de 64 bits y (2) instrucciones vectorizadas en los enteros de 32 bits se procesan el doble que en los de 64 bits. ¿Sigue siendo realmente el más rápido?
Matthieu M.
4
Más rápido para algunas cosas, más lento para otras. No existe una respuesta única para "cuál es el tamaño más rápido de número entero" cuando se consideran las matrices frente a la necesidad de extensión cero. En System V x86-64, ABI uint32_fast_tes un tipo de 64 bits, por lo que guarda la extensión de signo ocasional y permite, en imul rax, [mem]lugar de una instrucción de carga de extensión cero separada, cuando se usa con números enteros o punteros de 64 bits. Pero eso es todo lo que obtiene por el precio de duplicar la huella de caché y el tamaño del código adicional (REX prefijado en todo)
Peter Cordes
1
Además, la división de 64 bits es mucho más lenta que la división de 32 bits en la mayoría de las CPU x86, y algunas (como la familia Bulldozer, Atom y Silvermont) tienen una multiplicación de 64 bits más lenta que 32. La familia Bulldozer también tiene 64 bits más lentos popcnt . Y recuerde, solo es seguro usar este tipo para valores de 32 bits, porque es más pequeño en otras arquitecturas, por lo que está pagando este costo por nada.
Peter Cordes
2
Esperaría que, como promedio ponderado de todas las aplicaciones C y C ++, hacer uint32_fast_ten x86 sea una elección terrible. Las operaciones que son más rápidas son pocas y distantes entre sí, y el beneficio cuando ocurren son en su mayoría minúsculas: las diferencias para el imul rax, [mem]caso que menciona @PeterCordes son muy , muy pequeñas: una sola uop en el dominio fusionado y cero en el dominio no fusionado. En la mayoría de los escenarios interesantes, ni siquiera agregará un solo ciclo. Equilibre eso con el doble del uso de memoria y una peor vectorización, es difícil ver que gane con mucha frecuencia.
BeeOnRope
2
@PeterCordes - interesante pero también terrible :). Lo haría fast_taún peor int: no solo tiene diferentes tamaños en diferentes plataformas, sino que tendría diferentes tamaños dependiendo de las decisiones de optimización y diferentes tamaños en diferentes archivos. Como cuestión práctica, creo que no puede funcionar incluso con la optimización del programa completo: los tamaños en C y C ++ son fijos sizeof(uint32_fast_t)o cualquier cosa que lo determine, incluso directamente, siempre debe devolver el mismo valor, por lo que sería muy difícil para el compilador hacer tal transformación.
BeeOnRope
25

¿Por qué mucha gente usa en uint32_tlugar de uint32_fast_t?

Respuesta tonta:

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

Respuesta práctica:

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

Respuesta pragmática:

  • Mucha gente simplemente no sabe (o simplemente no les importa) uint_fast32_t , como se demuestra en los comentarios y respuestas, y probablemente asumen que es simple unsigned inttener la misma semántica, aunque muchas arquitecturas actuales todavía tienen 16 bits inty algunas muestras raras de Museum tienen otros tamaños de int extraños menores de 32.

Respuesta de UX:

  • Aunque posiblemente más rápido que uint32_t, uint_fast32_tes más lento de usar: lleva más tiempo escribir, especialmente teniendo en cuenta la búsqueda de ortografía y semántica en la documentación de C ;-)

La elegancia importa, (obviamente basada en opiniones):

  • uint32_tSe ve lo suficientemente mal como para que muchos programadores prefieran definir el suyo propio u32o el uint32tipo ... Desde esta perspectiva, uint_fast32_tparece torpe más allá de la reparación. No es de extrañar que se sienta en el banco con sus amigos uint_least32_ty demás.
chqrlie
fuente
+1 para UX. Es mejor de lo std::reference_wrapperque supongo, pero a veces me pregunto si el comité de estándares realmente quiere que se utilicen los tipos que estandariza ...
Matthieu M.
7

Una razón es que unsigned intya 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, simplemente use el fundamental into el unsigned inttipo.
Si bien el estándar no garantiza explícitamente que sea más rápido, indirectamente lo hace indicando "Los enteros simples 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 firmar) es con lo que el procesador se siente más cómodo.

Ahora, por supuesto, no sabes qué tamaño unsigned intpodría ser. Solo sabes que es al menos tan grande como short(y creo recordar que shortdebe tener al menos 16 bits, ¡aunque ahora no puedo encontrar eso en el estándar!). Por lo general, son 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 he encontrado con una arquitectura en la que este fuera 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, intera muy claramente de 16 bits en ese entonces).

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

uint32_t, según el estándar C, garantiza que obtiene exactamente 32 bits. 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_tgarantiza que sea cual sea el tamaño, no puede ser menor de 32 bits (pero muy bien podría ser mayor). A veces, pero mucho más raramente que un witdh exacto o "no me importa", esto es lo que quieres.

Por último, uint_fast32_tes algo superfluo en mi opinión, excepto para propósitos de documentación de intención. El estándar C dice "designa un tipo de entero que suele ser el más rápido" (tenga en cuenta la palabra "normalmente") y menciona explícitamente que no es necesario que sea el más rápido para todos los propósitos. En otras palabras, uint_fast32_tes casi lo mismo que uint_least32_t, que también suele ser el más rápido, solo que sin garantía (pero no garantía de ninguna manera).

Dado que la mayoría de las veces no te importa el tamaño exacto o quieres exactamente 32 (o 64, a veces 16) bits, y dado que el tipo "no importa" unsigned intes el más rápido de todos modos, esto explica por qué uint_fast32_tno es así. usado frecuentemente.

Damon
fuente
3
Me sorprende que no recuerde procesadores de 16 bits inten 8 bits, no puedo recordar ninguno de esos días que usaba algo más grande. Si la memoria funciona, los compiladores para la arquitectura x86 segmentada también utilizaron 16 bits int.
Mark Ransom
@MarkRansom: Vaya, tienes razón. Estaba taaaaaaaaaaaaaaaaaaaaaaaaaaan convencido de que intel 68000 tenía 32 bits (lo que pensé, como ejemplo). No fue ...
Damon
intestaba destinado a ser el tipo más rápido en el pasado con un ancho mínimo de 16 bits (es por eso que C tiene una regla de promoción de enteros), pero hoy con arquitecturas de 64 bits esto ya no es cierto. Por ejemplo, los enteros de 8 bytes son más rápidos que los enteros de 4 bytes en x86_64 bit porque con enteros de 4 bytes el compilador tiene que insertar instrucciones adicionales que expanden el valor de 4 bytes en un valor de 8 bytes antes de compararlo con otros valores de 8 bytes.
StaceyGirl
"unsigned int" no es necesariamente el más rápido en x64. Pasaron cosas raras.
Joshua
Otro caso común es que long, por razones históricas, debe ser de 32 bits y intahora se requiere que no sea más ancho que long, por lo que es intposible que deba permanecer en 32 bits incluso cuando 64 bits serían más rápidos.
Davislor
6

No he visto evidencia que uint32_tse utilice para su rango. En cambio, la mayoría de las veces que he visto uint32_tse usa para contener exactamente 4 octetos de datos en varios algoritmos, ¡con semántica de cambio y envolvente garantizada!

También hay otras razones para usar en uint32_tlugar de uint_fast32_t: A menudo es que proporcionará ABI estable. Además, el uso de la memoria se puede conocer con precisión. Esto contrarresta en gran medida lo que sea la ganancia de velocidad uint_fast32_t, siempre que ese tipo sea distinto del de uint32_t.

Para valores <65536, ya existe un tipo útil, se llama unsigned int( unsigned shortse requiere que tenga al menos ese rango también, pero unsigned intes del tamaño de palabra nativa) Para valores <4294967296, hay otro llamado unsigned long.


Y, por último, la gente no lo usa uint_fast32_tporque es muy largo de escribir y fácil de escribir mal: D

Antti Haapala
fuente
@ikegami: cambiaste mi intención con la shortedición. intes presumiblemente el rápido cuando es distinto de short.
Antti Haapala
1
Entonces, tu última oración está completamente equivocada. Afirmar que debería usar en unsigned intlugar de uint16_fast_tsignifica que afirma saber más que el compilador.
ikegami
Además, mis disculpas por cambiar la intención de su texto. Esa no era mi intención.
ikegami
unsigned longno es una buena opción si su plataforma tiene 64 bits longy solo necesita números <2^32.
Ruslan
1
@ikegami: El tipo "unsigned int" siempre se comportará como un tipo unsigned, incluso cuando se promocione. En este sentido es superior a ambos uint16_ty uint_fast16_t. Si uint_fast16_tse especificaran más libremente que los tipos enteros normales, de modo que su rango no necesita ser consistente para los objetos cuyas direcciones no se toman, eso podría ofrecer algunos beneficios de rendimiento en plataformas que realizan aritmética de 32 bits internamente pero tienen un bus de datos de 16 bits. . Sin embargo, la Norma no permite tal flexibilidad.
supercat
5

Muchas rasones.

  1. Mucha gente no sabe que existen los tipos "rápidos".
  2. Es más detallado escribir.
  3. Es más difícil razonar sobre el comportamiento de sus programas cuando no conoce el tamaño real del tipo.
  4. En realidad, el estándar no determina el más rápido, ni tampoco qué tipo es realmente más rápido puede depender mucho del contexto.
  5. No he visto evidencia de que los desarrolladores de plataformas hayan pensado en el tamaño de estos tipos al definir sus plataformas. Por ejemplo, en Linux x86-64, 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 más rápido para una aplicación determinada, necesita comparar su código en su compilador.

enchufar
fuente
Históricamente ha habido procesadores que tenían instrucciones de acceso a memoria de 32 bits y / o 64 bits, pero no de 8 y 16 bits. Así que int_fast {8,16} _t no habría sido del todo estúpido hace más de 20 años. AFAIK, el último procesador convencional de este tipo fue el DEC Alpha 21064 original (se mejoró la segunda generación 21164). Probablemente todavía haya DSP integrados o lo que sea que solo haga accesos de palabra, pero la portabilidad normalmente no es una gran preocupación en tales cosas, por lo que no veo por qué harías un culto rápido en esos. Y había máquinas Cray "todo es de 64 bits" construidas a mano.
user1998586
1
Categoría 1b: a muchas personas no les importa que existan los tipos "rápidos". Esa es mi categoría.
gnasher729
Categoría 6: Mucha gente no confía en que los tipos "rápidos" sean los más rápidos. Pertenezco a esa categoría.
Más claro
5

Desde el punto de vista de la corrección y la facilidad de codificación, uint32_ttiene muchas ventajas sobreuint_fast32_t en particular, debido al tamaño definido con mayor precisión y la semántica aritmética, como muchos usuarios han señalado anteriormente.

Lo que quizás se ha pasado por alto es la supuesta ventaja de uint_fast32_tque puede ser más rápido , pero nunca se materializó de manera significativa. La mayoría de los procesadores de 64 bits que han dominado la era de los 64 bits (x86-64 y Aarch64 en su mayoría) evolucionaron a partir de arquitecturas de 32 bits y tienen operaciones nativas rápidas de 32 bits incluso en el modo de 64 bits. Así que uint_fast32_tes lo mismo que uint32_ten esas plataformas.

Incluso si algunas de las plataformas "también ejecutadas" como POWER, MIPS64, SPARC solo ofrecen operaciones ALU de 64 bits, la gran mayoría de las operaciones interesantes de 32 bits se pueden realizar sin problemas en registros de 64 bits: los de 32 bits inferiores lo harán tener los resultados deseados (y todas las plataformas principales al menos le permiten cargar / almacenar 32 bits). El desplazamiento a la izquierda es el principal problema, pero incluso eso se puede optimizar en muchos casos mediante optimizaciones de seguimiento de valor / rango en el compilador.

Dudo que el desplazamiento a la izquierda ocasionalmente un poco más lento o la multiplicación 32x32 -> 64 supere el doble del uso de memoria para tales valores, en todas las aplicaciones excepto 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 recuento / velocidad de instrucciones (a favor de uint_fast32_t), incluso eso no me queda claro. Sí, en algunas plataformas necesitará instrucciones adicionales para algunas operaciones de 32 bits, pero también guardará algunas instrucciones porque:

  • El uso de un tipo más pequeño a menudo permite al compilador combinar inteligentemente operaciones adyacentes usando una operación de 64 bits para lograr dos operaciones de 32 bits. Un ejemplo de este tipo de "vectorización del pobre" no es infrecuente. Por ejemplo, la creación de una constante struct two32{ uint32_t a, b; }en me raxgusta two32{1, 2} se puede optimizar en una sola, mov rax, 0x20001mientras que la versión de 64 bits necesita dos instrucciones. En principio, esto también debería ser posible para operaciones aritméticas adyacentes (misma operación, diferente operando), pero no lo he visto en la práctica.
  • Un menor "uso de memoria" a menudo conduce a menos instrucciones, incluso si la memoria o la huella de caché no son un problema, debido a que se copia cualquier estructura de tipo o matrices de este tipo, se obtiene el doble de rentabilidad por cada registro copiado.
  • Los tipos de datos más pequeños a menudo aprovechan mejores convenciones de llamadas modernas como SysV ABI, que empaquetan datos de estructura de datos de manera eficiente en registros. Por ejemplo, puede devolver una estructura de hasta 16 bytes en los registros rdx:rax. Para una función que devuelve una estructura con 4 uint32_tvalores (inicializada a partir de una constante), eso se traduce en

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

    La misma estructura con 4 64 bits uint_fast32_tnecesita un movimiento de registro y cuatro tiendas en la memoria para hacer lo mismo (y la persona que llama probablemente tendrá que 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 aproximadamente el doble de densamente en los registros disponibles para los parámetros, por lo que es menos probable que se quede sin argumentos de registro y tenga que pasar a la pila 1 .

  • Incluso si elige usarlo uint_fast32_tpara lugares donde "la velocidad importa", a menudo también tendrá lugares donde necesite un tipo de tamaño fijo. Por ejemplo, al pasar valores para salida externa, de entrada externa, como parte de su ABI, como parte de una estructura que necesita un diseño específico, o porque usa inteligentemente uint32_tpara grandes agregaciones de valores para ahorrar espacio en la memoria. En los lugares donde sus uint_fast32_ttipos y `` uint32_t` necesitan interactuar, puede encontrar (además de la complejidad del desarrollo), extensiones de signos innecesarias u otro código relacionado con la discrepancia de tamaño. Los compiladores hacen un buen trabajo optimizando esto en muchos casos, pero aún no es inusual ver esto en la 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 empaquetar estructuras de manera ajustada en registros no siempre es una clara ventaja para valores más pequeños. Significa que es posible que los valores más pequeños tengan que "extraerse" antes de que puedan utilizarse. Por ejemplo, una función simple que devuelve la suma de los dos miembros de la estructura juntos necesita un mov rax, rdi; shr rax, 32; add edi, eaxtiempo para la versión de 64 bits, cada argumento obtiene su propio registro y solo necesita un solo addo lea. Aún así, si acepta que el diseño de "empaquetar las estructuras firmemente al pasar" tiene sentido en general, los valores más pequeños aprovecharán más esta característica.

BeeOnRope
fuente
glibc en x86-64 Linux usa 64 bits uint_fast32_t, lo cual es un error en mi opinión. (Aparentemente, Windows uint_fast32_tes un tipo de 32 bits en Windows). Al ser 64 bits en x86-64 Linux es la razón por la que nunca recomendaría que nadie use uint_fast32_t: está optimizado para un recuento bajo de instrucciones (los argumentos de función y los valores de retorno nunca necesitan extensión cero para utilizar como índice de matriz) no para la velocidad general o el tamaño del código en una de las principales plataformas importantes.
Peter Cordes
2
Ah, claro, leí su comentario anterior sobre el SysV ABI, pero como señalará más adelante, tal vez fue un grupo / documento diferente el que lo decidió, pero supongo que una vez que eso sucede, está bastante escrito. Creo que incluso es cuestionable que el recuento de ciclos puro / recuento de instrucciones favorezca los tipos más grandes, incluso ignorando los efectos de la huella de memoria y la vectorización, incluso en plataformas sin un buen soporte de operación de 32 bits, porque todavía hay casos en los que el compilador puede optimizar mejor los tipos más pequeños. Agregué algunos ejemplos arriba. @PeterCordes
BeeOnRope
SysV empaquetar varios miembros de estructura en el mismo registro cuesta más instrucciones con bastante frecuencia cuando se devuelve un pair<int,bool>o pair<int,int>. Si ambos miembros no son constantes en tiempo de compilación, normalmente hay más que un OR, y la persona que llama tiene que descomprimir los valores devueltos. ( bugs.llvm.org/show_bug.cgi?id=34840 LLVM optimiza el paso de valor de retorno para funciones privadas, y debería tratar el int de 32 bits como si tomara el todo, raxpor lo que boolestá separado en dllugar de necesitar una constante de 64 bits para testit.)
Peter Cordes
1
Creo que los compiladores generalmente no dividen funciones. Despegar una ruta rápida como una función separada es una optimización útil a nivel de fuente (especialmente en un encabezado donde se puede insertar). Puede ser muy bueno si el 90% de las entradas son el "caso de no hacer nada"; hacer ese filtrado en el bucle de la persona que llama es una gran victoria. IIRC, Linux usa __attribute__((noinline))exactamente para asegurarse de que gcc no alinee la función de manejo de errores y coloque un montón de push rbx/ .../ pop rbx/ ... en la ruta rápida de algunas funciones importantes del kernel que tienen muchas llamadas y no están alineadas.
Peter Cordes
1
En Java también es muy importante porque la inserción es muy importante para futuras optimizaciones (especialmente la desvirtualización, que es omnipresente a diferencia de C ++), por lo que a menudo vale la pena dividir una ruta rápida allí, y la "optimización del código de bytes" es en realidad una cosa (a pesar de la sabiduría convencional de que no tiene sentido porque el JIT hace la compilación final) solo para obtener la cuenta regresiva del código de bytes, ya que las decisiones de inserción se basan en el tamaño del código de bytes, no en el tamaño del código de máquina en línea (y la correlación puede variar en órdenes de magnitud).
BeeOnRope
4

A efectos prácticos, uint_fast32_tes completamente inútil. Está definido incorrectamente en la plataforma más extendida (x86_64) y realmente no ofrece ninguna ventaja en ningún lugar a menos que tenga un compilador de muy baja calidad. Conceptualmente, nunca tiene sentido utilizar los tipos "rápidos" en estructuras / matrices de datos: cualquier ahorro que obtenga del tipo que es más eficiente para operar será eclipsado por el costo (pérdidas de caché, etc.) de aumentar el tamaño de su conjunto de datos de trabajo. Y para las variables locales individuales (contadores de bucles, temporales, etc.), un compilador que no sea de juguete generalmente puede trabajar con un tipo más grande en el código generado si eso es más eficiente, y solo truncar al tamaño nominal cuando sea necesario para la corrección (y con tipos firmados, nunca es necesario).

La única variante que es teóricamente útil es uint_least32_t, para cuando necesita poder almacenar cualquier valor de 32 bits, pero quiere ser portátil a máquinas que carecen de un tipo de tamaño exacto de 32 bits. Prácticamente, hablando, sin embargo, eso no es algo de lo que deba preocuparse.

R .. GitHub DEJA DE AYUDAR A ICE
fuente
4

Según tengo entendido, intinicialmente se suponía que era un tipo entero "nativo" con una garantía adicional de que debería tener al menos 16 bits de tamaño, algo que se consideraba un tamaño "razonable" en ese entonces.

Cuando las plataformas de 32 bits se volvieron más comunes, podemos decir que el tamaño "razonable" ha cambiado a 32 bits:

  • Windows moderno usa 32 bits inten todas las plataformas.
  • POSIX garantiza que intsea ​​de al menos 32 bits.
  • C #, Java tiene un tipo intque se garantiza que sea exactamente de 32 bits.

Pero cuando la plataforma de 64 bits se convirtió en la norma, nadie se expandió intpara convertirse en un entero de 64 bits debido a:

  • Portabilidad: mucho código depende de inttener un tamaño de 32 bits.
  • Consumo de memoria: duplicar el uso de memoria para cada uno intpuede no ser razonable en la mayoría de los casos, ya que en la mayoría de los casos las cifras en uso son mucho menores que 2 mil millones.

Ahora, ¿por qué preferirías uint32_thacerlo uint_fast32_t? Por la misma razón, los lenguajes, C # y Java siempre usan números enteros de tamaño fijo: el programador no escribe código pensando en tamaños posibles de diferentes tipos, escribe para una plataforma y prueba el código en esa plataforma. La mayor parte del código depende implícitamente de tamaños específicos de tipos de datos. Y es por eso que uint32_tes una mejor opción para la mayoría de los casos: no permite ninguna ambigüedad con respecto a su comportamiento.

Además, ¿es uint_fast32_trealmente el tipo más rápido en una plataforma con un tamaño igual o superior a 32 bits? Realmente no. Considere este compilador de código de GCC para x86_64 en Windows:

extern uint64_t get(void);

uint64_t sum(uint64_t value)
{
    return value + get();
}

El ensamblado generado se ve así:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

Ahora, si cambia get()el valor de retorno a uint_fast32_t(que es 4 bytes en Windows x86_64) obtendrá esto:

push   %rbx
sub    $0x20,%rsp
mov    %rcx,%rbx
callq  d <sum+0xd>
mov    %eax,%eax        ; <-- additional instruction
add    %rbx,%rax
add    $0x20,%rsp
pop    %rbx
retq

Observe cómo el código generado es casi el mismo excepto por una mov %eax,%eaxinstrucción adicional después de la llamada a la función que está destinada a expandir el valor de 32 bits en un valor de 64 bits.

No existe tal problema si solo usa valores de 32 bits, pero probablemente usará aquellos con size_tvariables (¿tamaños de matriz probablemente?) Y esos son de 64 bits en x86_64. En Linux uint_fast32_tes de 8 bytes, por lo que la situación es diferente.

Muchos programadores usan intcuando necesitan devolver un valor pequeño (digamos en el rango [-32,32]). Esto funcionaría perfectamente si intfuera un tamaño entero nativo de la plataforma, pero como no está en plataformas de 64 bits, otro tipo que coincida con el tipo nativo de la plataforma es una mejor opción (a menos que se use con frecuencia con otros enteros de menor tamaño).

Básicamente, independientemente de lo que diga el estándar, de uint_fast32_ttodos modos está roto en algunas implementaciones. Si le preocupa la instrucción adicional generada en algunos lugares, debe definir su propio tipo de entero "nativo". O puede usarlo size_tpara este propósito, ya que generalmente coincidirá con el nativetamaño (no estoy incluyendo plataformas antiguas y oscuras como 8086, solo plataformas que pueden ejecutar Windows, Linux, etc.).


Otro signo que muestra que intse suponía que era un tipo entero nativo es "regla de promoción de enteros". La mayoría de las CPU solo pueden realizar operaciones en modo nativo, por lo que la CPU de 32 bits generalmente solo puede hacer sumas, restas, etc. de 32 bits (las CPU Intel son una excepción aquí). Los tipos enteros de otros tamaños solo se admiten mediante instrucciones de carga y almacenamiento. Por ejemplo, el valor de 8 bits debe cargarse con la instrucción apropiada "cargar 8 bits con signo" o "cargar 8 bits sin signo" y expandirá el valor a 32 bits después de la carga. Sin la regla de promoción de enteros, los compiladores C tendrían que agregar un poco más de código para las expresiones que usan tipos más pequeños que el tipo nativo. Desafortunadamente, esto ya no es válido con arquitecturas de 64 bits, ya que los compiladores ahora tienen que emitir instrucciones adicionales en algunos casos (como se mostró arriba).

StaceyChica
fuente
2
Los pensamientos sobre "nadie expandió int para que sea un entero de 64 bits porque" y "Desafortunadamente, esto ya no es válido con arquitecturas de 64 bits" son muy buenos puntos . Para ser justos sobre lo "más rápido" y comparar el código ensamblador: en este caso, parece que el segundo fragmento de código es más lento con su instrucción adicional, pero la longitud del código y la velocidad a veces no están tan bien correlacionadas. Una comparación más sólida informaría los tiempos de ejecución, pero eso no es tan fácil de hacer.
chux - Reincorporar a Monica
No creo que sea fácil medir la lentitud del segundo código, la CPU Intel podría estar haciendo un muy buen trabajo, pero un código más largo también significa una gran contaminación del caché. Una sola instrucción de vez en cuando probablemente no duele, pero la utilidad de uint_fast32_t se vuelve ambigua.
StaceyGirl
Estoy totalmente de acuerdo en que la utilidad de se uint_fast32_tvuelve ambigua, salvo en circunstancias muy selectas. Sospecho que la razón uint_fastN_tprincipal es acomodar el "no usemos unsignedcomo de 64 bits, aunque a menudo es más rápido en una nueva plataforma, porque se romperá demasiado código", pero "todavía quiero un tipo rápido de al menos N bits . " Te volvería a UV si pudiera.
chux - Reincorporar a Monica
La mayoría de las arquitecturas de 64 bits pueden operar fácilmente en enteros de 32 bits. Incluso DEC Alpha (que era una nueva arquitectura de 64 bits en una rama en lugar de una extensión de un ISA de 32 bits existente como PowerPC64 o MIPS64) tenía cargas / almacenes de 32 y 64 bits. (¡Pero no cargas / almacenes de bytes o de 16 bits!). La mayoría de las instrucciones eran solo de 64 bits, pero tenían soporte de HW nativo para agregar / suscribir y multiplicar de 32 bits que truncan el resultado a 32 bits. ( alasir.com/articles/alpha_history/press/alpha_intro.html ) Por lo tanto, casi no habría ganancia de velocidad al hacer int64 bits, y generalmente una pérdida de velocidad de la huella de caché.
Peter Cordes
Además, si creó int64 bits, su uint32_ttypedef de ancho fijo necesitaría uno __attribute__u otro truco, o algún tipo personalizado que sea más pequeño que int. (O short, pero luego tienes el mismo problema para uint16_t.) Nadie quiere eso. 32 bits es lo suficientemente ancho para casi todo (a diferencia de 16 bits); usar enteros de 32 bits cuando eso es todo lo que necesita no es "ineficiente" de ninguna manera significativa en una máquina de 64 bits.
Peter Cordes
2

En muchos casos, cuando un algoritmo funciona con una matriz de datos, la mejor forma de mejorar el rendimiento es minimizar el número de pérdidas de caché. Cuanto más pequeño sea cada elemento, más de ellos pueden caber en el caché. Esta es la razón por la que todavía se escribe mucho código para usar punteros de 32 bits en máquinas de 64 bits: no necesitan nada cercano a 4 GiB de datos, pero el costo de hacer todos los punteros y compensaciones necesita ocho bytes en lugar de cuatro. sería sustancial.

También hay algunas ABI y protocolos especificados para necesitar exactamente 32 bits, por ejemplo, direcciones IPv4. Eso es lo que uint32_trealmente significa: use exactamente 32 bits, independientemente de si eso es eficiente en la CPU o no. Estos solían declararse como longo unsigned long, lo que causó muchos problemas durante la transición de 64 bits. Si solo necesita un tipo sin signo que contenga números hasta al menos 2³²-1, esa ha sido la definición de unsigned longdesde que salió el primer estándar C. En la práctica, sin embargo, se asumió que un código antiguo longpodría contener cualquier puntero o desplazamiento de archivo o marca de tiempo, y se asumió que el código antiguo tenía exactamente 32 bits de ancho, por lo que los compiladores no necesariamente pueden hacer longlo mismo int_fast32_tsin romper demasiadas cosas.

En teoría, sería más apto para el futuro que un programa lo use uint_least32_t, y tal vez incluso cargar uint_least32_telementos en una uint_fast32_tvariable para los cálculos. ¡Una implementación que no tuviera ningún uint32_ttipo podría incluso declararse en conformidad formal con la norma! (Simplemente no sería capaz de compilar muchos programas existentes.) En la práctica, no hay arquitectura ya dónde int, uint32_ty uint_least32_tno son los mismos, y ninguna ventaja, actualmente , a la realización de uint_fast32_t. Entonces, ¿por qué complicar demasiado las cosas?

Sin embargo, mire la razón por la que todos los 32_ttipos debían existir cuando ya lo teníamos long, y verá que esas suposiciones han estallado en nuestras caras antes. Es posible que su código termine ejecutándose algún día en una máquina donde los cálculos de 32 bits de ancho exacto son más lentos que el tamaño de la palabra nativa, y hubiera sido mejor usarlo uint_least32_tpara almacenamiento y uint_fast32_tcálculo religiosamente. O si cruzas ese puente cuando llegas y solo quieres algo simple, ahí está unsigned long.

Davislor
fuente
Pero hay arquitecturas donde intno es de 32 bits, por ejemplo ILP64. No es que sean comunes.
Antti Haapala
¿No creo que ILP64 exista en tiempo presente? Varias páginas web afirman que "Cray" lo usa, todas las cuales citan la misma página de Unix.org de 1997, pero UNICOS a mediados de los 90 en realidad hizo algo más extraño y los Crays de hoy usan hardware Intel. Esa misma página afirma que las supercomputadoras de ETA usaban ILP64, pero que cerraron hace mucho tiempo. Wikipedia afirma que el puerto de Solaris de HAL a SPARC64 usaba ILP64, pero también han estado fuera de servicio durante años. CppReference dice que ILP64 solo se usó en algunos Unices de 64 bits. Por lo tanto, es relevante solo para algunos retrocomputadores muy esotéricos.
Davislor
Tenga en cuenta que si utiliza la "interfaz ILP64" de la biblioteca de kernel matemático de Intel hoy, inttendrá 32 bits de ancho. El tipo MKL_INTes lo que cambiará.
Davislor
1

Para dar una respuesta directa: creo que la verdadera razón por la que uint32_tse usa over uint_fast32_to uint_least32_tes simplemente que es más fácil de escribir y, debido a que es más corto, mucho más agradable de leer: si crea estructuras con algunos tipos, y algunos de ellos son uint_fast32_to similar, entonces a menudo es difícil alinearlos bien con intu boolotros tipos en C, que son bastante cortos (caso en cuestión: charvs. character). Por supuesto, no puedo respaldar esto con datos duros, pero las otras respuestas solo pueden adivinar la razón también.

En cuanto a razones técnicas para preferir uint32_t, no creo que las haya; cuando sea absolutamente necesario un int sin firmar exacto de 32 bits, entonces este tipo es su única opción estandarizada. En casi todos los demás casos, las otras variantes son técnicamente preferibles, específicamente, uint_fast32_tsi le preocupa la velocidad y uint_least32_tsi le preocupa el espacio de almacenamiento. El uso uint32_ten cualquiera de estos casos corre el riesgo de no poder compilar, ya que no se requiere que exista el tipo.

En la práctica, los uint32_ttipos y relacionados existen en todas las plataformas actuales, excepto algunos DSP muy raros (hoy en día) o implementaciones de broma, por lo que existe poco riesgo real al usar el tipo exacto. Del mismo modo, si bien puede encontrarse con penalizaciones de velocidad con los tipos de ancho fijo, ya no son (en las CPU modernas) paralizantes.

Por eso, creo, el tipo más corto simplemente gana en la mayoría de los casos, debido a la pereza del programador.

Recuerda Monica
fuente