A variável local não inicializada é o gerador de números aleatórios mais rápido?

329

Eu sei que a variável local não inicializada é um comportamento indefinido ( UB ), e também o valor pode ter representações de interceptação que podem afetar a operação adicional, mas às vezes eu quero usar o número aleatório apenas para representação visual e não usá-lo em outra parte do programa, por exemplo, defina algo com cores aleatórias em um efeito visual, por exemplo:

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

é mais rápido que

void updateEffect(){
    for(int i=0;i<1000;i++){
        star[i].setColor(rand()%255,rand()%255,rand()%255);
        star[i].setVisible(rand()%2==0?true:false);
    }
}

e também mais rápido que outro gerador de números aleatórios?

ggrr
fonte
88
+1 Esta é uma pergunta perfeitamente legítima. É verdade que, na prática, valores não inicializados podem ser aleatórios. O fato de que eles não são particularmente e que isso de UB não faz pedindo que ruim.
imallett
35
@imallett: Absolutamente. Essa é uma boa pergunta, e pelo menos um jogo antigo do Z80 (Amstrad / ZX Spectrum) usou seu programa como dados para configurar seu terreno. Portanto, existem precedentes. Não posso fazer isso hoje em dia. Os sistemas operacionais modernos tiram toda a diversão.
Bathsheba
81
Certamente o principal problema é que não é aleatório.
john
30
De fato, há um exemplo de uma variável não inicializada sendo usada como um valor aleatório, veja o desastre do Debian RNG (Exemplo 4 neste artigo ).
31815 PaperBirdMaster
31
Na prática - e acredite, eu faço muita depuração em várias arquiteturas - sua solução pode fazer duas coisas: ler registros não inicializados ou memória não inicializada. Agora, enquanto "não inicializado" significa aleatório de uma certa maneira, na prática provavelmente conterá a) zeros , b) valores repetidos ou consistentes (no caso de leitura de memória anteriormente ocupada por mídia digital) ou c) lixo consistente com um valor limitado definido (no caso de leitura de memória anteriormente ocupada por dados digitais codificados). Nenhuma dessas são fontes reais de entropia.
mg30rg 31/07/2015

Respostas:

299

Como outros observaram, esse é um comportamento indefinido (UB).

Na prática, (provavelmente) realmente (meio que) funcionará. A leitura de um registro não inicializado nas arquiteturas x86 [-64] realmente produzirá resultados de lixo e provavelmente não fará nada de ruim (em oposição a, por exemplo, Itanium, onde os registros podem ser sinalizados como inválidos , de modo que lê erros de propagação como NaN).

Existem dois problemas principais:

  1. Não será particularmente aleatório. Nesse caso, você está lendo da pilha, para obter o que estava lá anteriormente. Que pode ser efetivamente aleatória, completamente estruturada, a senha digitada há dez minutos ou a receita de biscoitos da sua avó.

  2. É uma má prática (maiúscula 'B') permitir que coisas como essa entrem no seu código. Tecnicamente, o compilador pode inserir reformat_hdd();toda vez que você lê uma variável indefinida. Não vai , mas você não deve fazê-lo de qualquer maneira. Não faça coisas inseguras. Os menos exceções que você faz, mais seguro você é com os erros acidentais todo o tempo.

O problema mais urgente do UB é que ele torna o comportamento de todo o programa indefinido. Os compiladores modernos podem usar isso para eliminar grandes partes do seu código ou até mesmo voltar no tempo . Brincar com o UB é como um engenheiro vitoriano desmantelando um reator nuclear ativo. Há um zilhão de coisas a dar errado, e você provavelmente não conhecerá metade dos princípios subjacentes ou da tecnologia implementada. Ele pode estar bem, mas você ainda não deve deixar que isso aconteça. Veja as outras respostas legais para obter detalhes.

Além disso, eu demiti-lo.

imallett
fonte
39
@ Potatoswatter: Os registros Itanium podem conter NaT (Not a Thing), que na verdade é um "registro não inicializado". No Itanium, a leitura de um registro quando você não o escreveu pode abortar seu programa (leia mais aqui: blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx ). Portanto, há uma boa razão pela qual a leitura de valores não inicializados é um comportamento indefinido. Também é provavelmente uma razão pela qual Itanium não é muito popular :)
tbleher
58
Eu realmente me oponho à noção "isso funciona". Mesmo que fosse verdade hoje, o que não é, pode mudar a qualquer momento devido a compiladores mais agressivos. O compilador pode substituir qualquer leitura unreachable()e excluir metade do seu programa. Isso acontece na prática também. Esse comportamento neutralizou completamente o RNG em alguma distribuição Linux, acredito; A maioria das respostas nesta pergunta parece assumir que um valor não inicializado se comporta como um valor. Isso é falso.
usr
25
Além disso, eu diria que você parece uma coisa boba de se dizer, assumindo boas práticas, isso deve ser pego na revisão de código, discutido e nunca deve acontecer novamente. Definitivamente, isso deve ser detectado, pois estamos usando os sinalizadores de aviso corretos, certo?
Shafik Yaghmour
17
@ Michael Na verdade, é. Se um programa tiver um comportamento indefinido a qualquer momento, o compilador poderá otimizar seu programa de maneira a afetar o código anterior que invoca o comportamento indefinido. Existem vários artigos e demonstrações de como isso pode ser incompreensível. Aqui está uma boa: blogs.msdn.com/b/oldnewthing/archive/2014/06/27/10537746.aspx (que inclui a parte do padrão que diz todas as apostas estão fora se qualquer caminho no seu programa invoca UB)
Tom Tanner
19
Essa resposta soa como se "invocar um comportamento indefinido fosse ruim em teoria, mas na verdade não o machucará muito na prática" . Isto é errado. A coleta de entropia de uma expressão que causaria UB pode (e provavelmente causará ) a perda de toda a entropia coletada anteriormente . Este é um risco sério.
Theodoros Chatzigiannakis
213

Deixe-me dizer isso claramente: não invocamos comportamentos indefinidos em nossos programas . Nunca é uma boa ideia, ponto final. Existem raras exceções a esta regra; por exemplo, se você é um implementador de biblioteca implementando offsetof . Se o seu caso se enquadra nessa exceção, você provavelmente já sabe disso. Nesse caso, sabemos que o uso de variáveis ​​automáticas não inicializadas é um comportamento indefinido .

Os compiladores se tornaram muito agressivos com otimizações em relação ao comportamento indefinido e podemos encontrar muitos casos em que o comportamento indefinido levou a falhas de segurança. O caso mais infame é provavelmente a remoção de verificação de ponteiro nulo do kernel do Linux, mencionada na minha resposta ao bug de compilação do C ++? onde uma otimização do compilador em torno de um comportamento indefinido transformou um loop finito em um infinito.

Podemos ler as Otimizações Perigosas e a Perda de Causalidade do CERT ( vídeo ) que dizem, entre outras coisas:

Cada vez mais, os criadores de compiladores estão aproveitando comportamentos indefinidos nas linguagens de programação C e C ++ para melhorar as otimizações.

Freqüentemente, essas otimizações estão interferindo na capacidade dos desenvolvedores de executar análises de causa-efeito em seu código-fonte, ou seja, analisando a dependência dos resultados posteriores aos resultados anteriores.

Consequentemente, essas otimizações estão eliminando a causalidade no software e estão aumentando a probabilidade de falhas, defeitos e vulnerabilidades do software.

Especificamente em relação a valores indeterminados, o relatório de defeitos padrão C 451: Instabilidade de variáveis ​​automáticas não inicializadas contribui para algumas leituras interessantes. Ainda não foi resolvido, mas introduz o conceito de valores instáveis, o que significa que a indeterminação de um valor pode se propagar através do programa e pode ter valores indeterminados diferentes em pontos diferentes do programa.

Não conheço nenhum exemplo em que isso ocorra, mas neste momento não podemos descartar isso.

Exemplos reais, não o resultado que você espera

É improvável que você obtenha valores aleatórios. Um compilador poderia otimizar completamente o loop ausente. Por exemplo, com este caso simplificado:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r ;
    }
}

O clang o otimiza ( veja ao vivo ):

updateEffect(int*):                     # @updateEffect(int*)
    retq

ou talvez obtenha todos os zeros, como neste caso modificado:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r%255 ;
    }
}

veja ao vivo :

updateEffect(int*):                     # @updateEffect(int*)
    xorps   %xmm0, %xmm0
    movups  %xmm0, 64(%rdi)
    movups  %xmm0, 48(%rdi)
    movups  %xmm0, 32(%rdi)
    movups  %xmm0, 16(%rdi)
    movups  %xmm0, (%rdi)
    retq

Ambos os casos são formas perfeitamente aceitáveis ​​de comportamento indefinido.

Observe que, se estivermos em um Itanium, podemos acabar com um valor de interceptação :

[...] se o registro tiver um valor especial, lendo as armadilhas do registro, exceto por algumas instruções [...]

Outras notas importantes

É interessante notar a variação entre gcc e clang observada no projeto UB Canaries sobre o quanto eles estão dispostos a aproveitar o comportamento indefinido em relação à memória não inicializada. O artigo observa ( ênfase minha ):

É claro que precisamos ser completamente claros conosco de que essa expectativa não tem nada a ver com o padrão de linguagem e tudo a ver com o que um compilador em particular faz, porque os fornecedores desse compilador não estão dispostos a explorar esse UB ou apenas porque eles ainda não conseguiram explorá-lo . Quando não existe garantia real do fornecedor do compilador, gostamos de dizer que os UBs ainda não explorados são bombas-relógio : eles estão esperando para sair no próximo mês ou no próximo ano, quando o compilador ficar um pouco mais agressivo.

Como Matthieu M. salienta, o que todo programador C deve saber sobre o comportamento indefinido nº 2/3 também é relevante para esta questão. Diz entre outras coisas ( ênfase minha ):

O importante e assustador é perceber que praticamente qualquer otimização baseada em comportamento indefinido pode começar a ser acionada no código de buggy a qualquer momento no futuro . Inlining, desenrolamento de loop, promoção de memória e outras otimizações continuarão melhorando, e uma parte significativa de sua razão de existir é expor otimizações secundárias como as acima.

Para mim, isso é profundamente insatisfatório, em parte porque o compilador inevitavelmente acaba sendo responsabilizado, mas também porque significa que enormes corpos de código C são minas terrestres apenas esperando para explodir.

Por uma questão de completude, eu provavelmente deveria mencionar que as implementações podem optar por tornar o comportamento indefinido bem definido; por exemplo, o gcc permite punções de tipo através de uniões enquanto em C ++ isso parece um comportamento indefinido . Se for esse o caso, a implementação deve documentá-lo e isso geralmente não será portátil.

Shafik Yaghmour
fonte
1
+ (int) (PI / 3) para os exemplos de saída do compilador; um exemplo da vida real de que UB é, bem, UB .
2
Utilizar o UB efetivamente costumava ser a marca registrada de um excelente hacker. Essa tradição já dura provavelmente 50 anos ou mais agora. Infelizmente, agora são necessários computadores para minimizar os efeitos do UB por causa de pessoas más. Eu realmente gostei de descobrir como fazer coisas legais com código de máquina UB ou leitura / gravação de porta, etc. Nos anos 90, quando o sistema operacional não era tão capaz de proteger o usuário de si mesmo.
Sfdcfox
1
@sfdcfox se você estivesse fazendo isso no código da máquina / montador, não era um comportamento indefinido (pode ter sido um comportamento não convencional).
Caleth
2
Se você tem uma montagem específica em mente, use-a e não escreva C. não compatível. Todos saberão que você está usando um truque não portátil específico. E não são as pessoas más que querem dizer que você não pode usar o UB, é a Intel, etc., fazendo seus truques no chip.
Caleth 02/08/2015
2
@ 500-InternalServerError porque eles podem não ser facilmente detectáveis ​​ou não podem ser detectáveis ​​no caso geral e, portanto, não há como desabilitá-los. O que é diferente das violações da gramática que podem ser detectadas. Também não temos um diagnóstico mal formado e mal formado, que geralmente separa programas mal formados que poderiam ser detectados em teoria daqueles que, em teoria, não podiam ser detectados com confiabilidade.
Shafik Yaghmour
164

Não, é terrível.

O comportamento de usar uma variável não inicializada é indefinido em C e C ++, e é muito improvável que esse esquema tenha propriedades estatísticas desejáveis.

Se você deseja um gerador de números aleatórios "rápido e sujo", então rand()é a sua melhor aposta. Na sua implementação, tudo o que faz é uma multiplicação, uma adição e um módulo.

O gerador mais rápido que conheço exige que você use a uint32_tcomo o tipo da variável pseudo-aleatória Ie use

I = 1664525 * I + 1013904223

para gerar valores sucessivos. Você pode escolher qualquer valor inicial I(chamado de semente ) que seja do seu agrado. Obviamente, você pode codificar essa linha. A envolvente garantida por padrão de um tipo não assinado atua como módulo. (As constantes numéricas são escolhidas a dedo pelo notável programador científico Donald Knuth.)

Bathsheba
fonte
9
O gerador "linear congruencial" que você apresenta é bom para aplicativos simples, mas apenas para aplicativos não criptográficos. É possível prever seu comportamento. Veja, por exemplo, " Decifrando uma criptografia congruencial linear ", do próprio Don Knuth (IEEE Transactions on Information Theory, Volume 31)
Jay
24
@ Jay comparado a uma variável unitializada para rápida e suja? Esta é uma solução muito melhor.
Mike McMahon
2
rand()não é adequado ao objetivo e deve ser totalmente reprovado, na minha opinião. Hoje em dia, você pode fazer o download de geradores de números aleatórios amplamente licenciados e amplamente superiores (por exemplo, Mersenne Twister), que são quase tão rápidos quanto com a maior facilidade, portanto não há necessidade de continuar usando o altamente defeituosorand()
Jack Aidley
1
O rand () tem outro problema terrível: ele usa um tipo de bloqueio, chamado de threads internas, diminui drasticamente o seu código. Pelo menos, há uma versão reentrante. E se você usa C ++ 11, a API aleatória fornece tudo o que você precisa.
Marwan Burelle
4
Para ser justo, ele não perguntou se era um bom gerador de números aleatórios. Ele perguntou se era rápido. Bem, sim, provavelmente é o jejum., Mas os resultados não serão muito aleatórios.
jcoder
42

Boa pergunta!

Indefinido não significa que é aleatório. Pense nisso: os valores que você obteria em variáveis ​​não inicializadas globais foram deixados lá pelo sistema ou por seus / outros aplicativos em execução. Dependendo do que o seu sistema faz com a memória não usada e / ou que tipo de valores o sistema e os aplicativos geram, você pode obter:

  1. Sempre o mesmo.
  2. Seja um de um pequeno conjunto de valores.
  3. Obtenha valores em um ou mais pequenos intervalos.
  4. Veja muitos valores dividíveis em 2/4/8 a partir de ponteiros no sistema 16/32 / 64 bits
  5. ...

Os valores que você obterá dependem completamente de quais valores não aleatórios são deixados pelo sistema e / ou aplicativos. Portanto, de fato haverá algum ruído (a menos que seu sistema limpe não mais a memória usada), mas o conjunto de valores do qual você extrairá nunca será aleatório.

As coisas pioram muito para as variáveis ​​locais, porque elas vêm diretamente da pilha do seu próprio programa. Há uma chance muito boa de que seu programa grave esses locais de pilha durante a execução de outro código. Eu estimo as chances de sorte nessa situação muito baixas, e uma alteração de código 'aleatória' que você faz tenta essa sorte.

Leia sobre aleatoriedade . Como você verá, a aleatoriedade é uma propriedade muito específica e difícil de obter. É um erro comum pensar que, se você pegar algo difícil de rastrear (como sua sugestão), terá um valor aleatório.

assuntos de significado
fonte
7
... e isso está deixando de fora todas as otimizações do compilador que estripariam completamente esse código.
Deduplicator
6 ... Você obterá "aleatoriedade" diferente em Debug and Release. Indefinido significa que você está fazendo errado.
Sql Surfer
Certo. Abreviaria ou resumiria com "indefinido"! = "Arbitrário"! = "Aleatório". Todos esses tipos de "incerteza" têm propriedades diferentes.
fche 21/06
É garantido que as variáveis ​​globais tenham um valor definido, explicitamente inicializado ou não. Isso definitivamente é verdade em C ++ e em C também .
Brian Vandenberg
32

Muitas boas respostas, mas permitam-me acrescentar outra e enfatizar que, em um computador determinístico, nada é aleatório. Isso ocorre tanto para os números produzidos por um pseudo-RNG quanto para os números aparentemente "aleatórios" encontrados em áreas da memória reservadas para variáveis ​​locais C / C ++ na pilha.

MAS ... há uma diferença crucial.

Os números gerados por um bom gerador de pseudoaleatórios têm propriedades que os tornam estatisticamente semelhantes aos empates verdadeiramente aleatórios. Por exemplo, a distribuição é uniforme. A duração do ciclo é longa: você pode obter milhões de números aleatórios antes que o ciclo se repita. A sequência não está correlacionada automaticamente: por exemplo, você não começará a ver padrões estranhos surgirem se você pegar cada segundo, terceiro ou 27º número ou se observar dígitos específicos nos números gerados.

Por outro lado, os números "aleatórios" deixados na pilha não possuem nenhuma dessas propriedades. Seus valores e sua aparente aleatoriedade dependem inteiramente de como o programa é construído, como é compilado e como é otimizado pelo compilador. A título de exemplo, aqui está uma variação da sua ideia como um programa independente:

#include <stdio.h>

notrandom()
{
        int r, g, b;

        printf("R=%d, G=%d, B=%d", r&255, g&255, b&255);
}

int main(int argc, char *argv[])
{
        int i;
        for (i = 0; i < 10; i++)
        {
                notrandom();
                printf("\n");
        }

        return 0;
}

Quando eu compilo esse código com o GCC em uma máquina Linux e o executo, ele se mostra bastante desagradável:

R=0, G=19, B=0
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255

Se você analisasse o código compilado com um desmontador, poderia reconstruir o que estava acontecendo, em detalhes. A primeira chamada para notrandom () usou uma área da pilha que não era usada por este programa anteriormente; quem sabe o que estava lá. Mas depois dessa chamada para notrandom (), há uma chamada para printf () (que o compilador GCC realmente otimiza para uma chamada para putchar (), mas não importa) e que substitui a pilha. Portanto, nos próximos momentos seguintes e subsequentes, quando notrandom () for chamado, a pilha conterá dados obsoletos da execução de putchar () e, como putchar () sempre é chamado com os mesmos argumentos, esses dados obsoletos sempre serão os mesmos, também.

Portanto, não há absolutamente nada aleatório nesse comportamento, nem os números obtidos dessa maneira têm nenhuma das propriedades desejáveis ​​de um gerador de números pseudoaleatórios bem escrito. De fato, na maioria dos cenários da vida real, seus valores serão repetitivos e altamente correlacionados.

De fato, como outros, eu também consideraria seriamente demitir alguém que tentou passar essa idéia como um "RNG de alto desempenho".

Viktor Toth
fonte
1
“Em um computador determinístico, nada é aleatório” - isso não é verdade. Os computadores modernos contêm todos os tipos de sensores que permitem produzir aleatoriedade verdadeira e imprevisível sem geradores de hardware separados. Em uma arquitetura moderna, os valores de /dev/randommuitas vezes são semeados de tais fontes de hardware e são de fato "ruído quântico", ou seja, verdadeiramente imprevisível no melhor sentido físico da palavra.
quer tocar hoje
2
Mas então, esse não é um computador determinístico, é? Agora você está contando com informações ambientais. De qualquer forma, isso nos leva muito além da discussão de um pseudo-RNG convencional vs. bits "aleatórios" na memória não inicializada. Além disso ... veja a descrição de / dev / random para avaliar o quão longe os implementadores foram para garantir que os números aleatórios sejam criptograficamente seguros ... precisamente porque as fontes de entrada não são ruído quântico puro e não correlacionado, mas ao contrário, leituras de sensor potencialmente altamente correlacionadas com apenas um pequeno grau de aleatoriedade. É bem lento também.
Viktor Toth
29

Comportamento indefinido significa que os autores dos compiladores são livres para ignorar o problema, porque os programadores nunca terão o direito de reclamar, aconteça o que acontecer.

Enquanto na teoria, ao entrar em terra UB, tudo pode acontecer (incluindo um daemon voando pelo nariz ), o que normalmente significa é que os autores do compilador simplesmente não se importam e, para variáveis ​​locais, o valor será o que estiver na memória da pilha naquele momento .

Isso também significa que frequentemente o conteúdo será "estranho", mas fixo ou levemente aleatório ou variável, mas com um padrão evidente claro (por exemplo, valores crescentes a cada iteração).

Com certeza você não pode esperar que seja um gerador aleatório decente.

6502
fonte
28

O comportamento indefinido é indefinido. Isso não significa que você obtenha um valor indefinido, significa que o programa pode fazer qualquer coisa e ainda atender à especificação de idioma.

Um bom compilador de otimização deve levar

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

e compilá-lo para um noop. Isso é certamente mais rápido do que qualquer alternativa. Ele tem o lado negativo de não fazer nada, mas esse é o lado negativo do comportamento indefinido.

Martijn
fonte
3
Depende muito se o objetivo de um compilador é ajudar os programadores a produzir arquivos executáveis ​​que atendam aos requisitos de domínio ou se o objetivo é produzir o executável mais "eficiente", cujo comportamento será consistente com os requisitos mínimos do Padrão C, sem considerar se esse comportamento servirá a algum propósito útil. Com relação ao objetivo anterior, fazer com que o código use alguns valores iniciais arbitrários para r, g, b ou acionar uma interceptação de depurador, se possível, seria mais útil do que transformar o código em um nop. No que diz respeito ao último objetivo ...
supercat 31/07
2
... um compilador ideal deve determinar quais entradas causariam a execução do método acima e eliminar qualquer código que só seria relevante quando essas entradas fossem recebidas.
Supercat 31/07
1
@supercat Ou seu objetivo pode ser C. produzir executáveis ​​eficientes em conformidade com a Norma, ajudando o programador a encontrar locais onde a conformidade pode não ser útil. Os compiladores podem atender a esse objetivo de compromisso emitindo mais diagnósticos do que o padrão exige, como os GCCs -Wall -Wextra.
Damian Yerrick
1
O fato de os valores não serem definidos não significa que o comportamento do código circundante seja indefinido. Nenhum compilador deve colocar essa função noop. As duas chamadas de função, quaisquer que sejam as entradas fornecidas, absolutamente DEVEM ser chamadas; o primeiro deve ser chamado com três números entre 0 e 255, e o segundo deve ser chamado com um valor verdadeiro ou falso. Um "bom compilador de otimização" poderia otimizar os parâmetros de função para valores estáticos arbitrários, livrando-se completamente das variáveis, mas isso é o máximo possível (bem, a menos que as próprias funções pudessem ser reduzidas a noops em determinadas entradas).
Dewi Morgan
@DewiMorgan - como as funções chamadas são do tipo "definir este parâmetro", elas quase certamente se reduzem a noops quando a entrada é igual ao valor atual do parâmetro, que o compilador está livre para assumir que é o caso.
Jules
18

Ainda não mencionado, mas os caminhos de código que invocam comportamentos indefinidos podem fazer o que o compilador desejar, por exemplo

void updateEffect(){}

O que é certamente mais rápido que o loop correto e, por causa do UB, é perfeitamente compatível.

Caleth
fonte
18

Por motivos de segurança, é necessário limpar a nova memória atribuída a um programa, caso contrário, as informações podem ser usadas e as senhas podem vazar de um aplicativo para outro. Somente quando você reutiliza a memória, você obtém valores diferentes de 0. E é muito provável que, em uma pilha, o valor anterior seja apenas fixo, porque o uso anterior dessa memória é fixo.

Arne
fonte
13

Seu exemplo de código específico provavelmente não faria o que você esperava. Enquanto tecnicamente cada iteração do loop recria as variáveis ​​locais para os valores r, g e b, na prática, é exatamente o mesmo espaço de memória na pilha. Portanto, ele não será randomizado novamente a cada iteração, e você acabará atribuindo os mesmos 3 valores para cada uma das 1000 cores, independentemente de quão aleatórios r, g e b sejam individual e inicialmente.

De fato, se funcionasse, eu ficaria muito curioso sobre o que o está re-randomizando. A única coisa em que consigo pensar é em uma interrupção intercalada que é empilhada no topo dessa pilha, altamente improvável. Talvez a otimização interna que as manteve como variáveis ​​de registro e não como locais de memória verdadeiros, onde os registros são reutilizados mais abaixo no loop, também ajudaria, especialmente se a função de visibilidade definida estiver com muita fome de registro. Ainda assim, longe de ser aleatório.

Jos
fonte
12

Como a maioria das pessoas aqui mencionou comportamento indefinido. Indefinido também significa que você pode obter algum valor inteiro válido (felizmente) e, nesse caso, isso será mais rápido (pois a chamada de função aleatória não é feita). Mas não use praticamente. Estou certo de que isso terá resultados terríveis, pois a sorte não está com você o tempo todo.

Ali Kazmi
fonte
1
Muito bom ponto! Pode ser um truque pragmático, mas que exige sorte.
significado-importa
1
Não há absolutamente nenhuma sorte envolvida. Se o compilador não otimizar o comportamento indefinido, os valores obtidos serão perfeitamente determinísticos (= dependem inteiramente do seu programa, de suas entradas, de seu compilador, das bibliotecas que ele usa, do tempo de seus encadeamentos, se houver encadeamentos). O problema é que você não pode raciocinar sobre esses valores, pois eles dependem dos detalhes da implementação.
cmaster - reinstate monica
Na ausência de um sistema operacional com uma pilha de tratamento de interrupções separada da pilha de aplicativos, a sorte pode estar envolvida, pois as interrupções freqüentemente perturbam o conteúdo da memória um pouco além do conteúdo atual da pilha.
31715
12

Muito ruim! Mau hábito, mau resultado. Considerar:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

Se a função A_Function_that_use_a_lot_the_Stack()sempre faz a mesma inicialização, ela deixa a pilha com os mesmos dados. Esses dados são o que chamamos updateEffect(): sempre o mesmo valor! .

Frankie_C
fonte
11

Eu realizei um teste muito simples, e não foi aleatório.

#include <stdio.h>

int main() {

    int a;
    printf("%d\n", a);
    return 0;
}

Toda vez que rodava o programa, ele imprimia o mesmo número ( 32767no meu caso) - você não pode ser muito menos aleatório do que isso. Isso é presumivelmente qualquer que seja o código de inicialização na biblioteca de tempo de execução deixado na pilha. Como ele usa o mesmo código de inicialização toda vez que o programa é executado, e nada mais varia no programa entre as execuções, os resultados são perfeitamente consistentes.

Barmar
fonte
Bom ponto. Um resultado depende fortemente de onde esse gerador de números "aleatórios" é chamado no código. É bastante imprevisível do que aleatório.
NO_NAME
10

Você precisa ter uma definição do que você quer dizer com 'aleatório'. Uma definição sensata envolve que os valores obtidos devem ter pouca correlação. Isso é algo que você pode medir. Também não é trivial alcançar de maneira controlada e reproduzível. Portanto, um comportamento indefinido certamente não é o que você está procurando.

Zsolt Szatmari
fonte
7

Há certas situações em que a memória não inicializada pode ser lida com segurança usando o tipo "char não assinado *" [por exemplo, um buffer retornado de malloc ]. O código pode ler essa memória sem ter que se preocupar com o compilador jogando a causalidade pela janela, e há momentos em que pode ser mais eficiente preparar o código para qualquer coisa que a memória possa conter do que garantir que dados não inicializados não sejam lidos ( um exemplo comum disso seria usar memcpyum buffer parcialmente inicializado, em vez de copiar discretamente todos os elementos que contêm dados significativos).

Mesmo nesses casos, no entanto, deve-se sempre supor que, se alguma combinação de bytes for particularmente vexatória, a leitura sempre produzirá esse padrão de bytes (e se um determinado padrão seria vexatório na produção, mas não no desenvolvimento, tal padrão não aparecerá até que o código esteja em produção).

A leitura da memória não inicializada pode ser útil como parte de uma estratégia de geração aleatória em um sistema incorporado, em que é possível ter certeza de que a memória nunca foi gravada com conteúdo substancialmente não aleatório desde a última vez em que o sistema foi ligado e se a fabricação O processo usado para a memória faz com que seu estado de inicialização varie de maneira semi-aleatória. O código deve funcionar mesmo que todos os dispositivos sempre produzam os mesmos dados, mas nos casos em que, por exemplo, um grupo de nós precisa selecionar IDs arbitrários e exclusivos o mais rápido possível, tendo um gerador "não muito aleatório" que fornece a metade dos nós a mesma inicial O ID pode ser melhor do que não ter nenhuma fonte inicial de aleatoriedade.

supercat
fonte
2
"se qualquer combinação de bytes for particularmente vexatória, a leitura sempre produzirá esse padrão de bytes" - até você codificar para lidar com esse padrão; nesse momento, ele não será mais vexatório e um padrão diferente será lido no futuro.
31715 Steve Steveop
@SteveJessop: Precisamente. Minha linha sobre desenvolvimento versus produção pretendia transmitir uma noção semelhante. O código não deve se importar com o que há na memória não inicializada além de uma vaga noção de "Alguma aleatoriedade pode ser legal". Se o comportamento do programa for afetado pelo conteúdo de uma parte da memória não inicializada, o conteúdo das partes adquiridas no futuro poderá ser afetado por isso.
Supercat
5

Como outros já disseram, será rápido, mas não aleatório.

O que a maioria dos compiladores fará por variáveis ​​locais é pegar algum espaço para elas na pilha, mas não se preocupe em defini-las para nada (o padrão diz que elas não precisam, por que diminuir o código que você está gerando?).

Nesse caso, o valor que você obterá dependerá do que estava anteriormente na pilha - se você chamar uma função antes desta com centenas de variáveis ​​de caracteres locais definidas como 'Q' e depois chamar sua função após como retorna, provavelmente você verá que seus valores "aleatórios" se comportam como se memset()todos eles tivessem um 'Q'.

Importante para a sua função de exemplo que tenta usar isso, esses valores não serão alterados toda vez que você os ler, eles serão os mesmos sempre. Assim, você terá 100 estrelas definidas com a mesma cor e visibilidade.

Além disso, nada indica que o compilador não deve inicializar esses valores - portanto, um compilador futuro poderá fazê-lo.

Em geral: má ideia, não faça. (como muitas otimizações de nível de código "inteligentes" realmente ...)

Alun Thomas
fonte
2
Você está fazendo algumas previsões fortes sobre o que acontecerá, embora nada disso seja garantido devido ao UB. Também não é verdade na prática.
usr
3

Como outros já mencionaram, esse é um comportamento indefinido ( UB ), mas pode "funcionar".

Exceto pelos problemas já mencionados por outros, vejo outro problema (desvantagem) - ele não funcionará em nenhum outro idioma que não seja C e C ++. Eu sei que esta pergunta é sobre C ++, mas se você pode escrever um código que será bom código C ++ e Java e não é um problema, por que não? Talvez um dia alguém tenha que portá-lo para outro idioma e procurar bugs causados ​​por "truques de mágica" como este definitivamente será um pesadelo (especialmente para um desenvolvedor C / C ++ inexperiente).

Aqui há dúvidas sobre outro UB semelhante. Imagine-se tentando encontrar um bug como esse sem saber sobre esse UB. Se você quiser ler mais sobre coisas tão estranhas em C / C ++, leia as respostas para a pergunta no link e veja esta GRANDE apresentação de slides. Isso o ajudará a entender o que está oculto e como está funcionando; não é apenas mais uma apresentação de slides cheia de "magia". Tenho certeza de que mesmo a maioria dos programadores experientes em C / c ++ pode aprender muito com isso.

cyriel
fonte
3

Não é uma boa idéia confiar nossa lógica no comportamento indefinido da linguagem. Além do que foi mencionado / discutido neste post, gostaria de mencionar que, com a abordagem / estilo C ++ modernos, esse programa pode não ser compilado.

Isso foi mencionado no meu post anterior, que contém a vantagem do recurso automático e do link útil para o mesmo.

https://stackoverflow.com/a/26170069/2724703

Portanto, se alterarmos o código acima e substituirmos os tipos reais por auto , o programa nem será compilado.

void updateEffect(){
    for(int i=0;i<1000;i++){
        auto r;
        auto g;
        auto b;
        star[i].setColor(r%255,g%255,b%255);
        auto isVisible;
        star[i].setVisible(isVisible);
    }
}
Mantosh Kumar
fonte
3

Eu gosto do seu modo de pensar. Realmente fora da caixa. No entanto, a troca realmente não vale a pena. A troca de memória e tempo de execução é uma coisa, inclusive o comportamento indefinido para o tempo de execução não é .

É necessário que você tenha uma sensação muito perturbadora ao saber que está usando algo "aleatório" como a lógica de negócios. Eu não faria isso.

DDan
fonte
3

Use 7757todos os lugares em que você for tentado a usar variáveis ​​não inicializadas. Eu escolhi aleatoriamente a partir de uma lista de números primos:

  1. é um comportamento definido

  2. é garantido que nem sempre será 0

  3. é primo

  4. é provável que seja tão estatisticamente aleatório quanto variáveis ​​não ritualizadas

  5. é provável que seja mais rápido que as variáveis ​​não inicializadas, pois seu valor é conhecido no momento da compilação

Glenn Teitelbaum
fonte
Para comparação, consulte os resultados nesta resposta: stackoverflow.com/a/31836461/2963099
Glenn Teitelbaum
1

Há mais uma possibilidade a considerar.

Compiladores modernos (ahem g ++) são tão inteligentes que passam pelo seu código para ver quais instruções afetam o estado e o que não afetam, e se uma instrução garantir que NÃO afeta o estado, o g ++ simplesmente removerá essa instrução.

Então aqui está o que vai acontecer. O g ++ definitivamente verá que você está lendo, executando aritmética, salvando, o que é essencialmente um valor de lixo, que produz mais lixo. Como não há garantia de que o novo lixo seja mais útil que o antigo, ele simplesmente eliminará seu loop. BLOOP!

Este método é útil, mas aqui está o que eu faria. Combine UB (comportamento indefinido) com velocidade rand ().

Obviamente, reduza rand()s executados, mas misture-os para que o compilador não faça nada que você não queira.

E eu não vou te despedir.

ps95
fonte
Acho muito difícil acreditar que um compilador possa decidir que seu código está fazendo algo bobo e removê-lo. Eu esperaria que ele otimizasse apenas o código não utilizado , e não o desaconselhável . Você tem um caso de teste reproduzível? De qualquer forma, a recomendação da UB é perigosa. Além disso, o GCC não é o único compilador competente que existe, por isso é injusto destacá-lo como "moderno".
underscore_d
-1

Usar dados não inicializados para aleatoriedade não é necessariamente uma coisa ruim se feito corretamente. De fato, o OpenSSL faz exatamente isso para propagar seu PRNG.

Aparentemente, esse uso não foi bem documentado, no entanto, porque alguém notou Valgrind reclamando sobre o uso de dados não inicializados e "corrigindo" os dados, causando um bug no PRNG .

Portanto, você pode fazê-lo, mas precisa saber o que está fazendo e garantir que qualquer pessoa que esteja lendo seu código entenda isso.

dbush
fonte
1
Isso dependerá do seu compilador, o que é esperado com um comportamento indefinido, como podemos ver na minha resposta que o clang hoje não fará o que eles querem.
Shafik Yaghmour
6
O fato de o OpenSSL ter usado esse método como uma entrada de entropia não diz que era bom. Afinal, a única outra fonte de entropia que eles usavam era o PID . Não é exatamente um bom valor aleatório. De alguém que confia em uma fonte de entropia tão ruim, não esperarei um bom julgamento sobre a outra fonte de entropia. Só espero que as pessoas que atualmente mantêm o OpenSSL sejam mais brilhantes.
cmaster - reinstate monica