A operação bit a bit resulta em tamanho variável inesperado


24

Contexto

Estamos portando código C que foi compilado originalmente usando um compilador C de 8 bits para o microcontrolador PIC. Um idioma comum que foi usado para impedir que variáveis ​​globais não assinadas (por exemplo, contadores de erro) retornem ao zero é o seguinte:

if(~counter) counter++;

O operador bit a bit aqui inverte todos os bits e a instrução só é verdadeira se counterfor menor que o valor máximo. Importante, isso funciona independentemente do tamanho da variável.

Problema

Agora, estamos direcionando um processador ARM de 32 bits usando o GCC. Percebemos que o mesmo código produz resultados diferentes. Até onde sabemos, parece que a operação de complemento bit a bit retorna um valor com um tamanho diferente do que seria de esperar. Para reproduzir isso, compilamos no GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

Na primeira linha de saída, obtemos o que esperávamos: ié 1 byte. No entanto, o complemento bit a bit de ina verdade é de quatro bytes, o que causa um problema, porque comparações com isso agora não fornecerão os resultados esperados. Por exemplo, se estiver fazendo (onde iestá devidamente inicializado uint8_t):

if(~i) i++;

Veremos i"envolver" de 0xFF para 0x00. Esse comportamento é diferente no GCC em comparação com quando ele costumava funcionar como pretendíamos no compilador anterior e no microcontrolador PIC de 8 bits.

Estamos cientes de que podemos resolver isso lançando da seguinte forma:

if((uint8_t)~i) i++;

Ou por

if(i < 0xFF) i++;

No entanto, em ambas as soluções alternativas, o tamanho da variável deve ser conhecido e é propenso a erros para o desenvolvedor de software. Esses tipos de verificação dos limites superiores ocorrem em toda a base de código. Existem vários tamanhos de variáveis (por exemplo., uint16_tE unsigned charetc.) e mudando estes em uma base de código outra forma de trabalho não é algo que nós estamos olhando para frente.

Questão

Nosso entendimento do problema está correto e existem opções disponíveis para resolver isso que não exigem uma nova visita a cada caso em que usamos esse idioma? Nossa suposição está correta, de que uma operação como complemento bit a bit deve retornar um resultado que é do mesmo tamanho que o operando? Parece que isso iria quebrar, dependendo da arquitetura do processador. Sinto que estou tomando pílulas malucas e que C deve ser um pouco mais portátil que isso. Novamente, nossa compreensão disso pode estar errada.

Na superfície, isso pode não parecer um grande problema, mas esse idioma que funcionava anteriormente é usado em centenas de locais e estamos ansiosos para entender isso antes de prosseguir com alterações caras.


Nota: Há uma pergunta duplicada aparentemente semelhante, mas não exata, aqui: A operação bit a bit no char fornece um resultado de 32 bits

Não vi o ponto crucial da questão discutido lá, ou seja, o tamanho do resultado de um complemento bit a bit sendo diferente do que é passado para o operador.


14
"Nossa suposição está correta, de que uma operação como complemento bit a bit deve retornar um resultado que é do mesmo tamanho que o operando?" Não, isso não está correto, promoções com números inteiros se aplicam.
— Thomas Jager

2
Embora certamente seja relevante, não estou convencido de que sejam duplicatas dessa questão em particular, porque não fornecem uma solução para o problema.
— Cody Gray

3
Sinto que estou tomando pílulas malucas e que o C deve ser um pouco mais portátil que isso. Se você não recebeu promoções com números inteiros em tipos de 8 bits, seu compilador não era compatível com o padrão C. Nesse caso, acho que você deve passar por todos os cálculos para verificá-los e corrigi-los, se necessário.
— user694733 16/04

11
Sou o único a pensar que lógica, além de contadores realmente sem importância, pode levá-la a "incrementar se houver espaço suficiente, senão esqueça"? Se você está portando código, pode usar int (4 bytes) em vez de uint_8? Isso impediria seu problema em muitos casos.
— puck

11
@ puck Você está certo, poderíamos alterá-lo para 4 bytes, mas isso quebraria a compatibilidade ao se comunicar com os sistemas existentes. A intenção é saber quando há algum erro e, portanto, um contador de 1 byte foi originalmente suficiente, e permanece.
— Charlie Salts

Respostas:


26

O que você está vendo é o resultado de promoções com números inteiros . Na maioria dos casos, em que um valor inteiro é usado em uma expressão, se o tipo do valor for menor do que into valor promovido int. Isso está documentado na seção 6.3.1.1p2 do padrão C :

O seguinte pode ser usado em uma expressão sempre que um intou unsigned intpode ser usado

  • Um objeto ou expressão com um tipo inteiro (diferente de intou unsigned int) cuja classificação de conversão inteira seja menor ou igual à classificação de inte unsigned int.
  • Um campo de bits do tipo _Bool, int ,assinado int , orsem sinal int`.

Se um intpuder representar todos os valores do tipo original (conforme restrito pela largura, para um campo de bits), o valor será convertido em um int; caso contrário, é convertido em um unsigned int. Estes são chamados de promoções de número inteiro . Todos os outros tipos são inalterados pelas promoções de número inteiro.

Portanto, se uma variável tiver o tipo uint8_te o valor 255, o uso de qualquer operador que não seja um elenco ou uma atribuição converterá primeiro em texto intcom o valor 255 antes de executar a operação. É por isso que sizeof(~i)você fornece 4 em vez de 1.

A Seção 6.5.3.3 descreve que promoções com números inteiros se aplicam ao ~operador:

O resultado do ~operador é o complemento bit a bit do seu operando (promovido) (ou seja, cada bit no resultado é definido se e somente se o bit correspondente no operando convertido não estiver definido). As promoções de números inteiros são realizadas no operando e o resultado tem o tipo promovido. Se o tipo promovido for um tipo não assinado, a expressão ~Eserá equivalente ao valor máximo representável nesse tipo menos E.

Portanto, assumindo um valor de 32 bits int, se countertiver o valor de 8 bits, 0xffele será convertido para o valor de 32 bits 0x000000ffe a aplicação ~será fornecida 0xffffff00.

Provavelmente, a maneira mais simples de lidar com isso é sem precisar saber o tipo é verificar se o valor é 0 após o incremento e, em caso afirmativo, diminuí-lo.

if (!++counter) counter--;

A envolvente de números inteiros não assinados funciona em ambas as direções, portanto, decrementar um valor de 0 fornece o maior valor positivo.


11
if (!++counter) --counter;para alguns programadores, pode ser menos estranho do que usar o operador de vírgula.
— Eric Postpischil 15/04

11
Outra alternativa é ++counter; counter -= !counter;.
— Eric Postpischil 15/04

@EricPostpischil Na verdade, eu gosto mais da sua primeira opção. Editado.
— dbush 15/04

15
Isso é feio e ilegível, não importa como você o escreva. Se você precisar usar um idioma como esse, faça um favor a todo programador de manutenção e o encerre como uma função embutida : algo como increment_unsigned_without_wraparoundou increment_with_saturation. Pessoalmente, eu usaria uma função genérica de três operandos clamp.
— Cody Gray

5
Além disso, você não pode fazer disso uma função, pois ela deve se comportar de maneira diferente para diferentes tipos de argumentos. Você precisaria usar uma macro de tipo genérico .
— user2357112 suporta Monica 16/04

7

em tamanho de (i); você solicita o tamanho da variável i , então 1

em tamanho de (~ i); você solicita o tamanho do tipo da expressão, que é um int , no seu caso 4


Usar

se (~ i)

para saber se eu não valorizo ​​255 (no seu caso com o uint8_t) não é muito legível, basta fazer

if (i != 255)

e você terá um código portátil e legível


Existem vários tamanhos de variáveis ​​(por exemplo, uint16_t e char não assinado etc.)

Para gerenciar qualquer tamanho de não assinado:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

A expressão é constante, então calculada em tempo de compilação.

#include <limits.h> para CHAR_BIT e #include <stdint.h> para uintmax_t


3
A pergunta afirma explicitamente que eles têm vários tamanhos para lidar, portanto, != 255é inadequada.
— Eric Postpischil 15/04

@EricPostpischil ah sim, esqueço isso, então "se (i! = ((1u << tamanhoof (i) * 8) - 1))" supondo sempre sem assinatura?
— bruno

11
Isso será indefinido para os unsignedobjetos, pois as mudanças na largura total do objeto não são definidas pelo padrão C, mas podem ser corrigidas com (2u << sizeof(i)*CHAR_BIT-1) - 1.
— Eric Postpischil 15/04

oh sim ofc, CHAR_BIT, meu mal
— bruno

2
Para segurança com tipos mais amplos, pode-se usar ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
— Eric Postpischil 15/04

5

Aqui estão várias opções para implementar "Adicionar 1 ao xclamp no valor máximo representável", considerando que xhá algum tipo de número inteiro não assinado:

  1. Adicione um se e somente se xfor menor que o valor máximo representável em seu tipo:

    x += x < Maximum(x);

    Consulte o item a seguir para a definição de Maximum. Esse método tem uma boa chance de ser otimizado por um compilador para obter instruções eficientes, como uma comparação, alguma forma de conjunto ou movimentação condicional e um complemento.

  2. Compare com o maior valor do tipo:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Isso calcula 2 N , onde N é o número de bits x, deslocando 2 por N -1 bits. Fazemos isso em vez de mudar 1 N bits, porque uma mudança no número de bits de um tipo não é definida por C A CHAR_BITmacro pode não ser familiar para alguns; é o número de bits em um byte, assim sizeof x * CHAR_BITcomo o número de bits no tipo de x.)

    Isso pode ser envolvido em uma macro, conforme desejado, para estética e clareza:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Incremente xe corrija se ele chegar a zero, usando um if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Incremente xe corrija se o valor é zero, usando uma expressão:

    ++x; x -= !x;

    Isso é nominalmente sem ramificação (às vezes benéfico para o desempenho), mas um compilador pode implementá-lo da mesma forma acima, usando uma ramificação, se necessário, mas possivelmente com instruções incondicionais, se a arquitetura de destino tiver instruções adequadas.

  5. Uma opção sem ramificação, usando a macro acima, é:

    x += 1 - x/Maximum(x);

    Se xfor o máximo de seu tipo, será avaliado como x += 1-1. Caso contrário, é x += 1-0. No entanto, a divisão é um pouco lenta em muitas arquiteturas. Um compilador pode otimizar isso para instruções sem divisão, dependendo do compilador e da arquitetura de destino.


11
Eu simplesmente não consigo votar novamente uma resposta que recomenda o uso de uma macro. C tem funções embutidas. Você não está fazendo nada dentro dessa definição de macro que não possa ser feito facilmente dentro de uma função embutida. E se você for usar uma macro, certifique-se de colocar parênteses estrategicamente para maior clareza: o operador << tem precedência muito baixa. Clang alerta sobre isso com -Wshift-op-parentheses. A boa notícia é que um compilador otimizador não irá gerar uma divisão aqui, então você não precisa se preocupar com a lentidão.
— Cody Gray

11
@CodyGray, se você acha que pode fazer isso com uma função, escreva uma resposta.
— Carsten S

2
@CodyGray: sizeof xnão pode ser implementado dentro de uma função C porque xteria que ser um parâmetro (ou outra expressão) com algum tipo fixo. Não foi possível produzir o tamanho de qualquer tipo de argumento usado pelo chamador. Uma lata de macro.
— Eric Postpischil 16/04

2

Antes de stdint.h, os tamanhos das variáveis ​​podem variar de compilador para compilador e os tipos de variáveis ​​reais em C ainda são int, long, etc, e ainda são definidos pelo autor do compilador quanto ao seu tamanho. Não existem algumas premissas padrão nem específicas. O (s) autor (es) precisa criar stdint.h para mapear os dois mundos; esse é o objetivo do stdint.h para mapear o uint_this que int, long, short.

Se você está portando código de outro compilador e ele usa char, short, int, long, é necessário passar por cada tipo e fazer a porta você mesmo, não há como contorná-lo. E você acaba com o tamanho certo para a variável, a declaração muda, mas o código, conforme escrito, funciona ....

if(~counter) counter++;

ou ... forneça a máscara ou a impressão direta diretamente

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

No final do dia, se você deseja que esse código funcione, é necessário portá-lo para a nova plataforma. Sua escolha sobre como. Sim, você precisa gastar o tempo necessário em cada caso e fazê-lo corretamente, caso contrário, continuará voltando a esse código, que é ainda mais caro.

Se você isolar os tipos de variáveis ​​no código antes de portar e qual o tamanho dos tipos de variáveis, isole as variáveis ​​que fazem isso (deve ser fácil grep) e altere suas declarações usando as definições stdint.h que esperamos que não sejam alteradas no futuro, e você ficaria surpreso, mas os cabeçalhos errados são usados ​​algumas vezes, então faça check-ins para que você possa dormir melhor à noite

if(sizeof(uint_8)!=1) return(FAIL);

E embora esse estilo de codificação funcione (se (~ contador) contador ++;), para a portabilidade desejar agora e no futuro, é melhor usar uma máscara para limitar especificamente o tamanho (e não confiar na declaração), faça isso quando o o código é escrito em primeiro lugar ou apenas termina a porta e você não precisará reportá-lo novamente outro dia. Ou, para tornar o código mais legível, faça o if x <0xFF then ou x! = 0xFF ou algo assim, o compilador pode otimizá-lo no mesmo código que faria para qualquer uma dessas soluções, apenas o torna mais legível e menos arriscado ...

Depende da importância do produto ou de quantas vezes você deseja enviar patches / atualizações ou rolar um caminhão ou caminhar até o laboratório para corrigir a questão de tentar encontrar uma solução rápida ou apenas tocar nas linhas de código afetadas. se é apenas uma centena ou poucas que não é tão grande de uma porta.


0
6.5.3.3 Operadores aritméticos unários
...
4 O resultado do ~operador é o complemento bit a bit do seu operando (promovido) (ou seja, cada bit no resultado é definido se e somente se o bit correspondente no operando convertido não estiver definido ) As promoções de números inteiros são realizadas no operando e o resultado tem o tipo promovido . Se o tipo promovido for um tipo não assinado, a expressão ~Eserá equivalente ao valor máximo representável nesse tipo menos E.

Rascunho Online do C 2011

O problema é que o operando de ~está sendo promovido para intantes da aplicação do operador.

Infelizmente, acho que não há uma maneira fácil de resolver isso. Escrita

if ( counter + 1 ) counter++;

não ajudará porque as promoções também se aplicam a ele. A única coisa que posso sugerir é criar algumas constantes simbólicas para o valor máximo que você deseja que esse objeto represente e teste com relação a isso:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;

Agradeço o argumento sobre a promoção inteira - parece que esse é o problema que estamos enfrentando. Vale ressaltar, no entanto, que no seu segundo exemplo de código, o -1não é necessário, pois isso faria com que o contador se estabelecesse em 254 (0xFE). De qualquer forma, essa abordagem, como mencionado na minha pergunta, não é ideal devido a diferentes tamanhos de variáveis ​​na base de código que participam desse idioma.
— Charlie Salts
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.