As extensões C notáveis ​​incluem tipos inteiros cujo comportamento é independente do tamanho da palavra da máquina

12

Uma característica interessante de C em comparação com outros idiomas é que muitos de seus tipos de dados são baseados no tamanho da palavra da arquitetura de destino, em vez de serem especificados em termos absolutos. Embora isso permita que o idioma seja usado para escrever código em máquinas que podem ter dificuldade com certos tipos, torna muito difícil projetar código que será executado de maneira consistente em diferentes arquiteturas. Considere o código:

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

Em uma arquitetura em que inthá 16 bits (ainda é verdade para muitos microcontroladores pequenos), esse código atribui o valor 1 usando um comportamento bem definido. Nas máquinas com int64 bits, atribuiria um valor 4294836225, novamente usando um comportamento bem definido. Em máquinas com int32 bits, provavelmente atribuiria um valor -131071 (não sei se isso seria Comportamento definido pela implementação ou indefinido). Mesmo que o código não use nada, exceto os que são nominalmente do tipo "tamanho fixo", o padrão exigiria que dois tipos diferentes de compilador em uso hoje produzissem dois resultados diferentes, e muitos compiladores populares hoje produzissem um terceiro.

Esse exemplo em particular é um tanto artificial, pois eu não esperaria que no código do mundo real atribuísse o produto de dois valores de 16 bits diretamente a um valor de 64 bits, mas foi escolhido como um breve exemplo para mostrar três maneiras inteiras as promoções podem interagir com tipos não assinados de tamanho supostamente fixo. Existem algumas situações do mundo real em que é necessário que a matemática em tipos não assinados seja executada de acordo com as regras da aritmética matemática inteira, outras em que é necessário que seja executada de acordo com as regras da aritmética modular e outras em que realmente não '' não importa. Muitos códigos do mundo real para coisas como somas de verificação baseiam-se no uint32_tmodelo de empacotamento aritmético 2³² e na capacidade de executar arbitrariamenteuint16_t aritmética e obter resultados que são, no mínimo, definidos como sendo o mod preciso 65536 (em oposição a desencadear um comportamento indefinido).

Embora essa situação pareça claramente indesejável (e se torne ainda mais à medida que o processamento de 64 bits se torna a norma para muitos propósitos), o comitê de padrões C do que observei prefere introduzir recursos de linguagem que já são usados ​​em algumas produções notáveis ambientes, em vez de inventá-los "do zero". Existem extensões notáveis ​​para a linguagem C que permitam ao código especificar não apenas como um tipo será armazenado, mas também como ele deve se comportar em cenários que envolvam possíveis promoções? Eu posso ver pelo menos três maneiras pelas quais uma extensão do compilador pode resolver esses problemas:

  1. Adicionando uma diretiva que instruiria o compilador a forçar certos tipos inteiros "fundamentais" a terem determinados tamanhos.

  2. Adicionando uma diretiva que instruiria o compilador a avaliar vários cenários de promoção como se os tipos da máquina tivessem tamanhos específicos, independentemente dos tamanhos reais dos tipos na arquitetura de destino.

  3. Ao permitir meios de declarar tipos com características específicas (por exemplo, declarar que um tipo deve se comportar como um anel algébrico mod-65536, independentemente do tamanho da palavra subjacente, e não deve ser implicitamente conversível em outros tipos; a adição de a wrap32a intdeve gerar um O resultado do tipo, wrap32independentemente de ter intmais de 16 bits, ao adicionar um wrap32diretamente a, wrap16deve ser ilegal (já que nenhum deles pode ser convertido no outro).

Minha preferência seria a terceira alternativa, pois permitiria que mesmo máquinas com tamanhos incomuns de palavras trabalhem com muito código que espera que as variáveis ​​sejam "quebradas", como faria com tamanhos de potência de dois; o compilador pode precisar adicionar instruções de mascaramento de bits para fazer com que o tipo se comporte adequadamente, mas se o código precisar de um tipo que envolva o mod 65536, é melhor que o compilador gere esse mascaramento em máquinas que precisam dele do que sobrecarregar o código-fonte com ele ou simplesmente ter esse código inutilizável em máquinas onde essa máscara seria necessária. No entanto, estou curioso para saber se existem extensões comuns que atingiriam o comportamento portátil por qualquer um dos meios acima, ou por alguns meios em que não pensei.

Para esclarecer o que estou procurando, há algumas coisas; mais notavelmente:

  1. Embora existam muitas maneiras pelas quais o código pode ser escrito para garantir a semântica desejada (por exemplo, definir macros para executar cálculos em operandos não assinados de tamanho específico, de modo a produzir um resultado que explode ou não explique explicitamente) ou pelo menos evite indesejados semântica (por exemplo, defina condicionalmente um tipo wrap32_tpara uint32_tcompiladores nos quais a uint32_tnão seria promovida e calcule que é melhor o código que exige wrap32_tfalha na compilação nas máquinas onde esse tipo seria promovido do que executá-lo e gerar comportamento falso), se houver alguma maneira de escrever o código que seria mais favorável em futuras extensões de idioma, usá-lo seria melhor do que criar minha própria abordagem.

  2. Tenho algumas idéias bastante sólidas sobre como o idioma pode ser estendido para resolver muitos problemas de tamanho inteiro, permitindo que o código produza semânticas idênticas em máquinas com tamanhos de palavras diferentes, mas antes de gastar algum tempo significativo escrevendo-as, gostaria saber quais esforços nessa direção já foram empreendidos.

Eu não desejo de forma alguma ser menosprezado pelo Comitê de Padrões C ou pelo trabalho que eles produziram; Espero, no entanto, que dentro de alguns anos seja necessário fazer o código funcionar corretamente em máquinas onde o tipo de promoção "natural" teria 32 bits, bem como naqueles em que teria 64 bits. Eu acho que com algumas extensões modestas da linguagem (mais modestas do que muitas das outras alterações entre a C99 e a C14), seria possível não apenas fornecer uma maneira limpa de usar eficientemente as arquiteturas de 64 bits, mas também facilitar a interação com as máquinas de "tamanho incomum de palavras" que o padrão historicamente inclinou para trás para oferecer suporte [por exemplo, possibilitando que máquinas com um charcódigo de 12 bits executem um código que espera umuint32_tpara embrulhar mod 2³²]. Dependendo da direção que as extensões futuras tomarem, eu também esperaria que fosse possível definir macros que permitissem que o código escrito hoje seja utilizável nos compiladores de hoje em que os tipos inteiros padrão se comportam como "esperados", mas também nos futuros compiladores em que inteiros tipos seriam padrão se comportariam de maneira diferente, mas onde podem fornecer os comportamentos necessários.

supercat
fonte
4
@RobertHarvey Você tem certeza? Como eu entendo a promoção inteira , se intfor maior que uint16_t, os operandos da multiplicação seriam promovidos inte a multiplicação seria realizada como intmultiplicação, e o intvalor resultante seria convertido em int64_tpara a inicialização de who_knows.
3
@RobertHarvey Como? No código do OP, não há nenhuma menção de int, mas ainda foge em (Mais uma vez assumindo o meu entendimento do padrão C está correta.).
2
@RobertHarvey Claro que parece ruim, mas, a menos que você possa apontar dessa maneira, não estará contribuindo com nada dizendo "nah, você deve estar fazendo algo errado". A própria questão é como evitar a promoção inteira ou contornar seus efeitos!
3
@RobertHarvey: Um dos objetivos históricos do Comitê de Padrões C foi para torná-lo possível para quase qualquer máquina para ter um "compilador C", e ter as regras ser suficiente específica que compiladores independentemente desenvolvidos C para qualquer máquina-alvo específico seria ser principalmente intercambiável. Isso foi complicado pelo fato de as pessoas começarem a escrever compiladores C para muitas máquinas antes de os padrões serem elaborados, e o Comitê de Padrões não queria proibir os compiladores de fazer qualquer coisa em que o código existente pudesse confiar . Alguns aspectos bastante fundamentais do padrão ... #
308
3
... são como não são porque alguém tentou formular um conjunto de regras que "faziam sentido", mas porque o Comitê estava tentando definir tudo o que os compiladores escritos independentemente, que já existiam, tinham em comum. Infelizmente, essa abordagem levou a padrões simultaneamente vagos demais para permitir que os programadores especifiquem o que precisa ser feito, mas específicos demais para permitir que os compiladores "apenas façam".
Supercat3

Respostas:

4

Como a intenção típica de código como este

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

é realizar a multiplicação em 64 bits (o tamanho da variável em que o resultado é armazenado), a maneira usual de obter o resultado correto (independente da plataforma) é converter um dos operandos para forçar uma multiplicação de 64 bits:

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

Eu nunca encontrei nenhuma extensão C que torne esse processo automático.

Bart van Ingen Schenau
fonte
1
Minha pergunta não era como forçar a avaliação correta de uma expressão aritmética específica (dependendo do tipo de resultado desejado, lançar um operando uint32_tou usar uma macro definida como #define UMUL1616to16(x,y)((uint16_t)((uint16_t)(x)*(uint16_t)(y)))ou #define UMUL1616to16(x,y)((uint16_t)((uint32_t)(x)*(uint16_t)(y)))dependendo do tamanho de int), mas sim se há quaisquer padrões emergentes de como lidar com essas coisas de maneira útil, em vez de definir minhas próprias macros.
Supercat 3/14
Eu também deveria ter mencionado que, para coisas como cálculos de hash e soma de verificação, o objetivo geralmente é obter um resultado e truncá-lo para o tamanho dos operandos. A intenção típica de uma expressão como (ushort1*ushort2) & 65535useria executar a aritmética mod-65536 para todos os valores de operando. Lendo a lógica do C89, acho bastante claro que, embora os autores tenham reconhecido que esse código pode falhar em algumas implementações se o resultado exceder 2147483647, eles esperavam que essas implementações se tornassem cada vez mais raras. Esse código às vezes falha no gcc moderno, no entanto.
Supercat 6/17