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.

Charlie Salts
fonte
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.

dbush
fonte
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

bruno
fonte
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.

Eric Postpischil
fonte
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.

old_timer
fonte
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++;
John Bode
fonte
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