Qual é a justificativa para o bash shell não avisar sobre estouro aritmético, etc.?

9

Existem limites definidos para os recursos de avaliação aritmética do bashshell. O manual é sucinto sobre esse aspecto da aritmética da casca, mas afirma :

A avaliação é feita em números inteiros de largura fixa sem verificação de estouro, embora a divisão por 0 seja interceptada e sinalizada como um erro. Os operadores e sua precedência, associatividade e valores são os mesmos da linguagem C.

Qual número inteiro de largura fixa a que isso se refere é realmente sobre qual tipo de dados é usado (e as especificidades de por que isso está além disso), mas o valor limite é expresso /usr/include/limits.hdessa maneira:

#  if __WORDSIZE == 64
#   define ULONG_MAX     18446744073709551615UL
#  ifdef __USE_ISOC99
#  define LLONG_MAX       9223372036854775807LL
#  define ULLONG_MAX    18446744073709551615ULL

E depois que você souber disso, poderá confirmar esse estado de fato da seguinte maneira:

# getconf -a | grep 'long'
LONG_BIT                           64
ULONG_MAX                          18446744073709551615

Este é um número inteiro de 64 bits e isso se traduz diretamente no shell no contexto da avaliação aritmética:

# echo $(((2**63)-1)); echo $((2**63)); echo $(((2**63)+1)); echo $((2**64))
9223372036854775807        //the practical usable limit for your everyday use
-9223372036854775808       //you're that much "away" from 2^64
-9223372036854775807     
0
# echo $((9223372036854775808+9223372036854775807))
-1

Portanto, entre 2 63 e 2 64 -1, você obtém números inteiros negativos mostrando a que distância de ULONG_MAX você está 1 . Quando a avaliação atinge esse limite e transborda, por qualquer ordem que seja, você não recebe nenhum aviso e parte da avaliação é redefinida para 0, o que pode gerar um comportamento incomum com algo como exponenciação associativa correta, por exemplo:

echo $((6**6**6))                      0   // 6^46656 overflows to 0
echo $((6**6**6**6))                   1   // 6^(6^46656) = 6^0 = 1
echo $((6**6**6**6**6))                6   // 6^(6(6^46656)) = 6^(6^0) = 6^1
echo $((6**6**6**6**6**6))         46656   // 6^(6^(6^(6^46656))) = 6^6
echo $((6**6**6**6**6**6**6))          0   // = 6^6^6^1 = 0
...

O uso sh -c 'command'não muda nada, portanto, devo assumir que essa é uma saída normal e compatível. Agora que acho que tenho um entendimento básico, mas concreto, do alcance e limite aritmético e do que isso significa no shell para avaliação de expressão, pensei em poder rapidamente examinar quais tipos de dados os outros softwares no Linux usam. Eu usei algumas bashfontes que tive para complementar a entrada deste comando:

{ shopt -s globstar; for i in /path/to/source_bash-4.2/include/**/*.h /usr/include/**/*.h; do grep -HE '\b(([UL])|(UL)|())LONG|\bFLOAT|\bDOUBLE|\bINT' $i; done; } | grep -iE 'bash.*max'

bash-4.2/include/typemax.h:#    define LLONG_MAX   TYPE_MAXIMUM(long long int)
bash-4.2/include/typemax.h:#    define ULLONG_MAX  TYPE_MAXIMUM(unsigned long long int)
bash-4.2/include/typemax.h:#    define INT_MAX     TYPE_MAXIMUM(int)

Há mais saída com as ifinstruções e eu posso procurar por um comando como awktambém etc. Percebo que a expressão regular que usei não capta nada sobre ferramentas de precisão arbitrárias que tenho como bce dc.


Questões

  1. Qual é a razão para não avisá-lo (como awkfaz ao avaliar 2 ^ 1024) quando sua avaliação aritmética transborda? Por que os números inteiros negativos entre 2 63 e 2 64 -1 são expostos ao usuário final quando ele está avaliando algo?
  2. Eu li em algum lugar que algum sabor do UNIX pode alterar interativamente o ULONG_MAX? Alguém já ouviu falar disso?
  3. Se alguém alterar arbitrariamente o valor do número inteiro não assinado máximo em limits.he recompilar bash, o que podemos esperar que aconteça?

Nota

1. Eu queria ilustrar mais claramente o que vi, pois é uma coisa empírica muito simples. O que eu notei é que:

  • (a) Qualquer avaliação que dê <2 ^ 63-1 está correta
  • (b) Qualquer avaliação que dê => 2 ^ 63 até 2 ^ 64 fornece um número inteiro negativo:
    • O intervalo desse número inteiro é x a y. x = -9223372036854775808 e y = 0.

Considerando isso, uma avaliação semelhante a (b) pode ser expressa como 2 ^ 63-1 mais algo dentro de x..y. Por exemplo, se formos literalmente solicitados a avaliar (2 ^ 63-1) +100 002 (mas pode ser qualquer número menor que em (a)), obtemos -9223372036854675807. Estou apenas afirmando o óbvio, mas acho que isso também significa que as duas expressões a seguir:

  • (2 ^ 63-1) + 100 002 AND;
  • (2 ^ 63-1) + (LLONG_MAX - {o que o shell nos dá ((2 ^ 63-1) + 100 002), que é -9223372036854675807}) bem, usando valores positivos que temos;
    • (2 ^ 63-1) + (9223372036854775807 - 9223372036854675807 = 100 000)
    • = 9223372036854775807 + 100 000

são muito próximos mesmo. A segunda expressão é "2" além de (2 ^ 63-1) + 100 002, isto é, o que estamos avaliando. É isso que quero dizer com números inteiros negativos, mostrando a que distância de 2 ^ 64 você está. Quero dizer, com esses números inteiros negativos e conhecimento dos limites, bem, você não pode concluir a avaliação dentro do intervalo x..y no shell bash, mas pode em outro lugar - os dados são utilizáveis ​​até 2 ^ 64 nesse sentido (eu poderia acrescentar no papel ou em bc). Além disso, porém, o comportamento é semelhante ao de 6 ^ 6 ^ 6, pois o limite é atingido como descrito abaixo no Q ...


fonte
5
Meu palpite é que a lógica se resume a "o shell não é a ferramenta certa para a matemática". Ele não foi projetado para isso e não tenta lidar com isso normalmente, como você mostra. Inferno, a maioria das conchas nem lida com carros alegóricos!
terdon
@terdon Embora a maneira como o shell lide com os números neste caso seja exatamente igual a todos os idiomas de alto nível que já ouvi falar. Tipos inteiros são de tamanho fixo e podem transbordar.
goldilocks
@terdon De fato, como eu pesquisei isso desde o momento 6 ^ 6 ^ 6, percebi isso. Também adivinhei que a razão pela qual não consegui encontrar muito conteúdo foi porque isso tinha a ver com C ou mesmo com C99. Como não sou desenvolvedor nem técnico de TI, tenho que aceitar todo o conhecimento que embasa essas suposições. Certamente alguém que exige precisão arbitrária sabe sobre o tipo de dados, mas obviamente eu não sou essa pessoa :) (mas notei o comportamento do awk @ 2 ^ 53 + 1, ou seja, float double; apenas precisão e interno versus impressão etc. está além de mim !).
1
Se você quiser trabalhar com grandes números no shell, utilização bc, como por exemplo: $num=$(echo 6^6^6 | bc). Infelizmente, bccoloca quebras de linha, então você precisa num=$(echo $num | sed 's/\\\s//g')depois; se você fizer isso em um pipe, existem caracteres de nova linha reais, que são estranhos com sed, embora num=$(echo 6^6^3 | bc | perl -pne 's/\\\s//g')funcionem. Em ambos os casos, agora você tem um número inteiro que pode ser usado, por exemplo num2=$(echo "$num * 2" | bc),.
28814 goldilocks
1
... Alguém aqui apontou que você pode desativar esse recurso de quebra de linha bcconfigurando BC_LINE_LENGTH=0.
28814 goldilocks

Respostas:

11

Portanto, entre 2 ^ 63 e 2 ^ 64-1, você obtém números inteiros negativos mostrando a que distância de ULONG_MAX você está.

Não. Como você acha isso? Por seu próprio exemplo, o máximo é:

> max=$((2**63 - 1)); echo $max
9223372036854775807

Se "overflow" significou "você obtém números inteiros negativos mostrando a que distância de ULONG_MAX você está", se adicionarmos um a isso, não deveríamos obter -1? Mas ao invés:

> echo $(($max + 1))
-9223372036854775808

Talvez você queira dizer que este é um número que você pode adicionar $maxpara obter uma diferença negativa, pois:

> echo $(($max + 1 + $max))
-1

Mas isso de fato não se mantém verdadeiro:

> echo $(($max + 2 + $max))
0

Isso ocorre porque o sistema usa o complemento de dois para implementar números inteiros assinados. 1 O valor resultante de um estouro NÃO é uma tentativa de fornecer uma diferença, uma diferença negativa, etc. É literalmente o resultado de truncar um valor para um número limitado de bits e depois interpretá-lo como um inteiro assinado de complemento de dois . Por exemplo, o motivo $(($max + 1 + $max))aparece como -1, porque o valor mais alto no complemento de dois é todos os bits configurados, exceto o bit mais alto (que indica negativo); juntá-los basicamente significa carregar todos os bits para a esquerda, para que você acabe (se o tamanho fosse 16 bits e não 64):

11111111 11111110

O bit alto (sinal) agora está definido porque foi transferido na adição. Se você adicionar mais um (00000000 00000001) a isso, todos os bits serão configurados , o que no complemento de dois é -1.

Eu acho que isso responde parcialmente à segunda metade da sua primeira pergunta - "Por que os números inteiros negativos são expostos ao usuário final?". Primeiro, porque esse é o valor correto de acordo com as regras dos números complementares de dois bits de 64 bits. Essa é a prática convencional da maioria das (outras) linguagens de programação de alto nível de uso geral (não consigo pensar em uma que não faça isso), por isso bashé aderente à convenção. Qual é também a resposta para a primeira parte da primeira pergunta - "Qual é a lógica?": Essa é a norma na especificação de linguagens de programação.

WRT a segunda pergunta, eu não ouvi falar de sistemas que mudam interativamente ULONG_MAX.

Se alguém altera arbitrariamente o valor do número inteiro não assinado máximo em limits.h, recompila o bash, o que podemos esperar que aconteça?

Não faria nenhuma diferença na forma como a aritmética se sai, porque esse não é um valor arbitrário usado para configurar o sistema - é um valor de conveniência que armazena uma constante imutável refletindo o hardware. Por analogia, você pode redefinir c para 55 mph, mas a velocidade da luz ainda será de 186.000 milhas por segundo. c não é um número usado para configurar o universo - é uma dedução sobre a natureza do universo.

ULONG_MAX é exatamente o mesmo. É deduzido / calculado com base na natureza dos números de N bits. Mudá-lo limits.hseria uma péssima idéia se essa constante for usada em algum lugar, supondo que ela represente a realidade do sistema .

E você não pode mudar a realidade imposta pelo seu hardware.


1. Eu não acho que isso (o meio de representação de número inteiro) seja realmente garantido por bash, uma vez que depende da biblioteca C subjacente e o padrão C não garante isso. No entanto, é isso que é usado na maioria dos computadores modernos normais.

Cachinhos Dourados
fonte
Eu sou muito grato! Chegando a um acordo com o elefante na sala e pensando. Sim, na primeira parte, trata-se principalmente de palavras. Atualizei meu Q para mostrar o que eu quis dizer. Vou pesquisar por que o complemento de dois descreve um pouco do que vi e sua resposta é inestimável para entender isso! No que diz respeito ao UNIX Q, devo ter interpretado mal algo sobre ARG_MAX com o AIX aqui . Felicidades!
1
De fato, você pode usar o complemento de dois para determinar o valor, se tiver certeza de que está no intervalo> 2 * $max, conforme descreve. Meus pontos são: 1) esse não é o objetivo; 2) certifique-se de entender se você quer fazer isso; 3) não é muito útil por causa da aplicabilidade muito limitada; 4) conforme a nota de rodapé, não é realmente garantido que o sistema funcione. use o complemento de dois. Em resumo, tentar explorar isso no código do programa seria considerado uma prática muito ruim. Existem bibliotecas / módulos de "grande número" (para shells no POSIX bc) - use-os se for necessário.
goldilocks
Apenas recentemente, vi algo que alavancou o complemento dos dois para implementar uma ALU com um somador binário de 4 bits com IC de transporte rápido; houve até uma comparação com o complemento de alguém (para ver como estava errado). Sua explicação foi fundamental para que eu sou capaz de nomear e conectar o que vi aqui com o que foi discutido nesses vídeos , aumentando a chance de eu realmente entender todas as implicações abaixo da linha assim que tudo se encaixar. Obrigado novamente por isso! Felicidades!