Una característica interesante de C en comparación con otros lenguajes es que muchos de sus tipos de datos se basan en el tamaño de palabra de la arquitectura de destino, en lugar de especificarse en términos absolutos. Si bien esto permite que el lenguaje se use para escribir código en máquinas que pueden tener dificultades con ciertos tipos, hace que sea muy difícil diseñar código que se ejecute de manera consistente en diferentes arquitecturas. Considera el código:
uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;
En una arquitectura donde int
es de 16 bits (sigue siendo cierto para muchos microcontroladores pequeños), este código asignaría un valor de 1 usando un comportamiento bien definido. En máquinas con int
64 bits, asignaría un valor 4294836225, nuevamente utilizando un comportamiento bien definido. En máquinas con int
32 bits, es probable que asigne un valor de -131071 (no sé si sería un comportamiento definido o no definido por la implementación). Aunque el código no usa nada excepto lo que se supone que son tipos de "tamaño fijo", el estándar requeriría que dos tipos diferentes de compiladores en uso hoy produzcan dos resultados diferentes, y muchos compiladores populares de hoy en día producirán un tercero.
Este ejemplo en particular es algo artificial, ya que no esperaría que en el código del mundo real se asigne el producto de dos valores de 16 bits directamente a un valor de 64 bits, pero se eligió como un breve ejemplo para mostrar tres formas enteras Las promociones pueden interactuar con tipos sin signo de tamaño supuestamente fijo. Hay algunas situaciones del mundo real en las que es necesario que las matemáticas en tipos sin signo se realicen de acuerdo con las reglas de la aritmética de enteros matemáticos, otras en las que es necesario que se realicen de acuerdo con las reglas de la aritmética modular, y algunas en las que realmente no funciona. No importa Una gran cantidad de código del mundo real para cosas como sumas de comprobación se basa en el uint32_t
ajuste de envoltura aritmética 2³² y en la capacidad de realizar operaciones arbitrarias.uint16_t
aritmética y obtener resultados que, como mínimo, se definen como mod 65536 precisos (en lugar de desencadenar un comportamiento indefinido).
A pesar de que esta situación parecería claramente indeseable (y lo será aún más a medida que el procesamiento de 64 bits se convierta en la norma para muchos propósitos), el comité de estándares C, por lo que he observado, prefiere introducir características de lenguaje que ya se utilizan en alguna producción notable entornos, en lugar de inventarlos "desde cero". ¿Existen extensiones notables del lenguaje C que permitan al código especificar no solo cómo se almacenará un tipo sino también cómo debería comportarse en escenarios que involucran posibles promociones? Puedo ver al menos tres formas en que una extensión del compilador podría resolver estos problemas:
Al agregar una directiva que instruiría al compilador a forzar ciertos tipos enteros "fundamentales" para que sean de ciertos tamaños.
Al agregar una directiva que instruiría al compilador a evaluar varios escenarios de promoción como si los tipos de la máquina tuvieran tamaños particulares, independientemente de los tamaños reales de los tipos en la arquitectura de destino.
Al permitir medios para declarar tipos con características específicas (por ejemplo, declarar que un tipo debe comportarse como un anillo algebraico envolvente mod-65536, independientemente del tamaño de palabra subyacente, y no debe ser implícitamente convertible a otros tipos; agregar un
wrap32
aint
debería producir un El resultado del tipowrap32
independientemente de siint
es mayor de 16 bits, mientras que agregar unwrap32
directamente a unwrap16
debería ser ilegal (ya que ninguno podría convertirse al otro).
Mi propia preferencia sería la tercera alternativa, ya que permitiría que incluso las máquinas con tamaños de palabra inusuales trabajen con una gran cantidad de código que espera que las variables se "ajusten" como lo harían con tamaños de potencia de dos; Es posible que el compilador tenga que agregar instrucciones de enmascaramiento de bits para que el tipo se comporte adecuadamente, pero si el código necesita un tipo que envuelva el mod 65536, es mejor que el compilador genere tal enmascaramiento en las máquinas que lo necesitan que saturar el código fuente con él o simplemente tener dicho código inutilizable en máquinas donde se necesitaría dicho enmascaramiento. Sin embargo, tengo curiosidad por saber si hay extensiones comunes que logren un comportamiento portátil a través de cualquiera de los medios anteriores, o por algún medio que no haya pensado.
Para aclarar lo que estoy buscando, hay algunas cosas; más destacado:
Si bien hay muchas formas en que se podría escribir el código para garantizar la semántica deseada (por ejemplo, definir macros para realizar operaciones matemáticas en operandos sin signo de tamaño particular para obtener un resultado que explícitamente se envuelva o no) o al menos evite indeseados semántica (por ejemplo, defina condicionalmente un tipo
wrap32_t
para que estéuint32_t
en compiladores dondeuint32_t
no se promocionaría, y calcule que es mejor para el código que requierewrap32_t
fallar la compilación en máquinas donde ese tipo se promocionaría que ejecutarlo y producir un comportamiento falso), Si hay alguna forma de escribir el código que jugaría más favorablemente con futuras extensiones de lenguaje, usar eso sería mejor que idear mi propio enfoque.Tengo algunas ideas bastante sólidas sobre cómo se puede extender el lenguaje para resolver muchos problemas de tamaño entero, lo que permite que el código produzca semánticas idénticas en máquinas con diferentes tamaños de palabras, pero antes de dedicar un tiempo significativo a escribirlas me gustaría saber qué esfuerzos en esa dirección ya se han llevado a cabo.
No deseo de ninguna manera ser visto como menospreciador del Comité de Normas C o del trabajo que han producido; Sin embargo, espero que dentro de unos años sea necesario hacer que el código funcione correctamente en máquinas donde el tipo de promoción "natural" tendría 32 bits, así como aquellas en las que sería de 64 bits. Creo que con algunas extensiones modestas del lenguaje (más modestas que muchos de los otros cambios entre C99 y C14) sería posible no solo proporcionar una forma limpia de utilizar eficientemente las arquitecturas de 64 bits, sino que también facilitar la interacción con las máquinas de "tamaño de palabra inusual" que el estándar históricamente se ha inclinado hacia atrás para admitir [por ejemplo, haciendo posible que máquinas con un char
código de 12 bits ejecuten un código que espera unuint32_t
para envolver mod 2³²]. Dependiendo de la dirección que tomen las extensiones futuras, también esperaría que fuera posible definir macros que permitirían que el código escrito hoy sea utilizable en los compiladores de hoy donde los tipos enteros predeterminados se comporten como "esperados", pero también en futuros compiladores donde sea entero los tipos serían comportamientos predeterminados de manera diferente, pero donde pueden proporcionar los comportamientos requeridos.
fuente
int
es mayor queuint16_t
, los operandos de la multiplicación se promoverían aint
y la multiplicación se llevaría a cabo comoint
multiplicación, y elint
valor resultante se convertiríaint64_t
para la inicialización dewho_knows
.int
, pero todavía se cuela. (Nuevamente, suponiendo que mi comprensión del estándar C sea correcta).Respuestas:
Como la intención típica de un código como este
consiste en realizar la multiplicación en 64 bits (el tamaño de la variable en la que se almacena el resultado), la forma habitual de obtener el resultado correcto (independiente de la plataforma) es lanzar uno de los operandos para forzar una multiplicación de 64 bits:
Nunca he encontrado extensiones en C que hagan que este proceso sea automático.
fuente
uint32_t
o usar una macro definida como#define UMUL1616to16(x,y)((uint16_t)((uint16_t)(x)*(uint16_t)(y)))
o#define UMUL1616to16(x,y)((uint16_t)((uint32_t)(x)*(uint16_t)(y)))
dependiendo del tamaño deint
), sino más bien si hay cualquier estándar emergente sobre cómo manejar esas cosas de manera útil en lugar de definir mis propias macros.(ushort1*ushort2) & 65535u
sería realizar la aritmética mod-65536 para todos los valores de operando. Al leer la justificación de C89, creo que está bastante claro que, si bien los autores reconocieron que dicho código podría fallar en algunas implementaciones si el resultado superaba 2147483647, esperaban que dichas implementaciones se volvieran cada vez más raras. Sin embargo, dicho código a veces falla en el gcc moderno.