¿Las extensiones de C notables incluyen tipos enteros cuyo comportamiento es independiente del tamaño de palabra de la máquina?

12

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 intes 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 int64 bits, asignaría un valor 4294836225, nuevamente utilizando un comportamiento bien definido. En máquinas con int32 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_tajuste 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:

  1. Al agregar una directiva que instruiría al compilador a forzar ciertos tipos enteros "fundamentales" para que sean de ciertos tamaños.

  2. 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.

  3. 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 wrap32a intdebería producir un El resultado del tipo wrap32independientemente de si intes mayor de 16 bits, mientras que agregar un wrap32directamente a un wrap16deberí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:

  1. 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_tpara que esté uint32_ten compiladores donde uint32_tno se promocionaría, y calcule que es mejor para el código que requiere wrap32_tfallar 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.

  2. 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 charcódigo de 12 bits ejecuten un código que espera unuint32_tpara 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.

Super gato
fuente
44
@RobertHarvey ¿Estás seguro? Según entiendo la promoción de enteros , si intes mayor que uint16_t, los operandos de la multiplicación se promoverían a inty la multiplicación se llevaría a cabo como intmultiplicación, y el intvalor resultante se convertiría int64_tpara la inicialización de who_knows.
3
@RobertHarvey ¿Cómo? En el código de OP, no se menciona int, pero todavía se cuela. (Nuevamente, suponiendo que mi comprensión del estándar C sea correcta).
2
@RobertHarvey Claro que suena mal, pero a menos que puedas señalarlo de esa manera, no estás contribuyendo nada al decir "no, debes estar haciendo algo mal". ¡La pregunta es cómo evitar la promoción de números enteros o evitar sus efectos!
3
@RobertHarvey: Uno de los objetivos históricos de la Comisión de Normas de C ha sido hacer posible que casi cualquier máquina que tenga un "compilador de C", y tienen las reglas lo suficientemente específicos que los compiladores de C-desarrollados de forma independiente de cualquier máquina objetivo particular haría Ser mayormente intercambiable. Esto fue complicado por el hecho de que la gente comenzó a escribir compiladores de C para muchas máquinas antes de que se redactaran los estándares, y el Comité de Estándares no quería prohibir a los compiladores hacer nada en lo que el código existente pudiera confiar . Algunos aspectos fundamentales del estándar ...
supercat
3
... son como son porque nadie trató de formular un conjunto de reglas que "tenían sentido", sino porque el Comité estaba tratando de concretar todas las cosas que los compiladores escritos independientemente que ya existían tenían en común. Desafortunadamente, este enfoque ha llevado a estándares que son a la vez demasiado vagos para permitir que los programadores especifiquen lo que se necesita hacer, pero demasiado específicos para permitir que los compiladores "simplemente lo hagan".
supercat

Respuestas:

4

Como la intención típica de un código como este

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

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:

uint16_t ffff16 = 0xFFFF;
int64_t i_know = (int64_t)ffff16 * ffff16;

Nunca he encontrado extensiones en C que hagan que este proceso sea automático.

Bart van Ingen Schenau
fuente
1
Mi pregunta no era cómo forzar la evaluación correcta de una expresión aritmética particular (dependiendo del tipo de resultado que uno desee, ya sea lanzar un operando uint32_to 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 de int), 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.
supercat
También debería haber mencionado que, para cosas como los cálculos de suma de comprobación y hashing, el propósito a menudo será tomar un resultado y truncarlo al tamaño de los operandos. La intención típica de una expresión como (ushort1*ushort2) & 65535userí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.
supercat