La variabile locale non inizializzata è il generatore di numeri casuali più veloce?


329

So che la variabile locale non inizializzata è un comportamento indefinito ( UB ), e anche il valore può avere rappresentazioni di trap che possono influire su ulteriori operazioni, ma a volte voglio usare il numero casuale solo per la rappresentazione visiva e non le userò ulteriormente in altre parti di programma, ad esempio, imposta qualcosa con un colore casuale in un effetto visivo, ad esempio:

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);
    }
}

è più veloce di

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 anche più veloce di altri generatori di numeri casuali?


88
+1 Questa è una domanda perfettamente legittima. È vero che in pratica i valori non inizializzati potrebbero essere casuali. Il fatto che non siano particolarmente e che sia UB non fa chiedere così male.
imallett,

35
@imallett: Assolutamente. Questa è una buona domanda, e almeno un vecchio gioco Z80 (Amstrad / ZX Spectrum) in passato ha usato il suo programma come dati per impostare il suo terreno. Quindi ci sono anche precedenti. Non posso farlo in questi giorni. I moderni sistemi operativi tolgono tutto il divertimento.
Bathsheba,

81
Sicuramente il problema principale è che non è casuale.
Giovanni,

30
In effetti, c'è un esempio di una variabile non inizializzata usata come valore casuale, vedere il disastro di Debian RNG (Esempio 4 in questo articolo ).
PaperBirdMaster,

31
In pratica - e credetemi, faccio molto debug su varie architetture - la vostra soluzione può fare due cose: leggere registri non inizializzati o memoria non inizializzata. Ora, mentre "non inizializzato" significa casuale in un certo modo, in pratica con molta probabilità conterrà a) zero , b) valori ripetitivi o coerenti (nel caso di lettura di memoria precedentemente occupata da media digitali) ec) immondizia consistente con un valore limitato set (in caso di lettura di memoria precedentemente occupata da dati digitali codificati). Nessuna di queste sono vere fonti di entropia.
mg30rg

Risposte:


299

Come altri hanno notato, questo è Undefined Behaviour (UB).

In pratica, funzionerà (probabilmente) effettivamente (in qualche modo). La lettura da un registro non inizializzato su architetture x86 [-64] produrrà effettivamente risultati spazzatura e probabilmente non farà nulla di male (al contrario di Itanium, dove i registri possono essere contrassegnati come non validi , in modo che le letture propagano errori come NaN).

Vi sono tuttavia due problemi principali:

  1. Non sarà particolarmente casuale. In questo caso, stai leggendo dallo stack, quindi otterrai tutto ciò che c'era in precedenza. Che potrebbe essere effettivamente casuale, completamente strutturato, la password che hai inserito dieci minuti fa o la ricetta dei biscotti di tua nonna.

  2. È una cattiva (maiuscola 'B') lasciare che cose del genere si insinuino nel tuo codice. Tecnicamente, il compilatore potrebbe inserire reformat_hdd();ogni volta che leggi una variabile non definita. Non lo farà , ma non dovresti farlo comunque. Non fare cose pericolose. Meno eccezioni fai, più sarai sicuro di errori accidentali in ogni momento.

Il problema più urgente con UB è che rende indefinito il comportamento dell'intero programma. I compilatori moderni possono usarlo per eludere enormi parti del codice o persino per tornare indietro nel tempo . Giocare con UB è come un ingegnere vittoriano che smantella un reattore nucleare in tensione. Ci sono milioni di cose che vanno male e probabilmente non conoscerai la metà dei principi sottostanti o della tecnologia implementata. Si potrebbe andare bene, ma ancora non dovrebbe lasciare che accada. Guarda le altre belle risposte per i dettagli.

Inoltre, ti licenzierei.


39
@Potatoswatter: i registri Itanium possono contenere NaT (Not a Thing) che in effetti è un "registro non inizializzato". Su Itanium, leggere da un registro quando non l'hai scritto può interrompere il tuo programma (leggi di più qui: blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx ). Quindi c'è una buona ragione per cui leggere valori non inizializzati è un comportamento indefinito. Probabilmente è anche uno dei motivi per cui Itanium non è molto popolare :)
Più tardi il

58
Sono davvero contrario all'idea "it kind of works". Anche se fosse vero oggi, cosa che non lo è, potrebbe cambiare in qualsiasi momento a causa di compilatori più aggressivi. Il compilatore può sostituire qualsiasi lettura con unreachable()ed eliminare metà del programma. Questo succede anche in pratica. Questo comportamento ha neutralizzato completamente l'RNG in alcune distro Linux credo; La maggior parte delle risposte a questa domanda sembrano assumere che un valore non inizializzato si comporti come un valore. Questo è falso.
usr

25
Inoltre, vorrei che tu sembri una cosa piuttosto sciocca da dire, supponendo che le buone pratiche dovrebbero essere prese in esame, discusse e non dovrebbero mai più accadere. Questo dovrebbe sicuramente essere preso dal momento che stiamo usando i flag di avviso corretti, giusto?
Shafik Yaghmour,

17
@Michael In realtà, lo è. Se un programma ha un comportamento indefinito in qualsiasi momento, il compilatore può ottimizzare il programma in modo tale da influire sul codice che precede quello che richiama il comportamento indefinito. Ci sono vari articoli e dimostrazioni di come la mente possa diventare sbalorditiva. Ecco una buona idea : blogs.msdn.com/b/oldnewthing/archive/2014/06/27/10537746.aspx (che include il bit nello standard che dice tutte le scommesse sono disattivate se qualsiasi percorso nel tuo programma invoca UB)
Tom Tanner

19
Questa risposta fa sembrare che "invocare un comportamento indefinito sia male in teoria, ma in realtà non ti farà molto male" . È sbagliato. La raccolta di entropia da un'espressione che provocherebbe (e probabilmente lo farà ) l' UB può causare la perdita di tutta l'entropia precedentemente raccolta . Questo è un grave pericolo.
Theodoros Chatzigiannakis,

213

Lasciatemelo dire chiaramente: non invochiamo comportamenti indefiniti nei nostri programmi . Non è mai una buona idea, punto. Ci sono rare eccezioni a questa regola; ad esempio, se sei un implementatore di librerie che implementa offsetof . Se il tuo caso rientra in tale eccezione, probabilmente lo sai già. In questo caso sappiamo che l'utilizzo di variabili automatiche non inizializzate è un comportamento indefinito .

I compilatori sono diventati molto aggressivi con l'ottimizzazione del comportamento indefinito e possiamo trovare molti casi in cui il comportamento indefinito ha portato a difetti di sicurezza. Il caso più infame è probabilmente la rimozione del controllo del puntatore null del kernel Linux che menziono nella mia risposta al bug di compilazione C ++? dove un'ottimizzazione del compilatore attorno a comportamenti indefiniti trasformava un ciclo finito in uno infinito.

Possiamo leggere le Ottimizzazioni pericolose di CERT e la perdita di causalità ( video ) che dice, tra le altre cose:

Sempre più spesso, gli autori di compilatori stanno sfruttando comportamenti indefiniti nei linguaggi di programmazione C e C ++ per migliorare le ottimizzazioni.

Spesso, queste ottimizzazioni interferiscono con la capacità degli sviluppatori di eseguire analisi di causa-effetto sul loro codice sorgente, ovvero analizzare la dipendenza dei risultati a valle dai risultati precedenti.

Di conseguenza, queste ottimizzazioni stanno eliminando la causalità nel software e stanno aumentando la probabilità di guasti, difetti e vulnerabilità del software.

In particolare per quanto riguarda i valori indeterminati, il rapporto sui difetti standard C 451: instabilità di variabili automatiche non inizializzate rende alcune letture interessanti. Non è stato ancora risolto, ma introduce il concetto di valori traballanti, il che significa che l'indeterminatezza di un valore può propagarsi attraverso il programma e può avere diversi valori indeterminati in diversi punti del programma.

Non conosco esempi in cui ciò accada, ma a questo punto non possiamo escluderlo.

Esempi reali, non il risultato che ti aspetti

È improbabile che tu ottenga valori casuali. Un compilatore potrebbe ottimizzare del tutto il loop. Ad esempio, con questo caso semplificato:

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

clang lo ottimizza via ( vederlo dal vivo ):

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

o forse ottenere tutti gli zeri, come in questo caso modificato:

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

vederlo dal 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

Entrambi questi casi sono forme perfettamente accettabili di comportamento indefinito.

Nota, se siamo su un Itanium potremmo finire con un valore trap :

[...] se il registro contiene uno speciale valore non-cosa-cosa, leggendo le trap del registro tranne alcune istruzioni [...]

Altre note importanti

È interessante notare la varianza tra gcc e clang osservata nel progetto UB Canaries su quanto siano disposti a trarre vantaggio da un comportamento indefinito rispetto alla memoria non inizializzata. L'articolo osserva ( enfasi il mio ):

Ovviamente dobbiamo essere completamente chiari con noi stessi sul fatto che una simile aspettativa non ha nulla a che fare con lo standard linguistico e tutto ciò che ha a che fare con un determinato compilatore, sia perché i provider di quel compilatore non sono disposti a sfruttare quell'UB o semplicemente perché non sono ancora riusciti a sfruttarlo . Quando non esiste alcuna garanzia reale da parte del fornitore del compilatore, ci piace dire che gli UB non ancora sfruttati sono bombe a orologeria : stanno aspettando di esplodere il mese prossimo o l'anno prossimo quando il compilatore diventerà un po 'più aggressivo.

Come sottolinea Matthieu M., ciò che ogni programmatore C dovrebbe sapere sul comportamento indefinito n. 2/3 è rilevante anche per questa domanda. Dice tra l'altro ( enfasi la mia ):

La cosa importante e spaventoso da capire è che quasi ogni ottimizzazione basata sul comportamento indefinito può iniziare l'attivazione, sul codice buggy in qualsiasi momento in futuro . Inline, srotolamento continuo, promozione della memoria e altre ottimizzazioni continueranno a migliorare, e una parte significativa della loro ragione di esistere è esporre ottimizzazioni secondarie come quelle sopra.

Per me questo è profondamente insoddisfacente, in parte perché il compilatore finisce inevitabilmente per essere incolpato, ma anche perché significa che enormi corpi di codice C sono mine di terra che aspettano solo di esplodere.

Per completezza, dovrei probabilmente menzionare che le implementazioni possono scegliere di rendere ben definito il comportamento indefinito, ad esempio gcc consente la punzonatura del tipo attraverso i sindacati mentre in C ++ sembra un comportamento indefinito . In tal caso, l'implementazione dovrebbe documentarlo e questo di solito non sarà portatile.


1
+ (int) (PI / 3) per gli esempi di output del compilatore; un esempio di vita reale che UB è, beh, UB .

2
L'uso di UB era in effetti il ​​marchio di un eccellente hacker. Questa tradizione dura ormai da 50 anni o più. Sfortunatamente, i computer sono ora tenuti a minimizzare gli effetti di UB a causa di Bad People. Mi è davvero piaciuto capire come fare cose interessanti con il codice macchina UB o lettura / scrittura delle porte, ecc. Negli anni '90, quando il sistema operativo non era in grado di proteggere l'utente da se stessi.
sfdcfox

1
@sfdcfox se lo facevi in ​​codice macchina / assemblatore, non era un comportamento indefinito (potrebbe essere stato un comportamento non convenzionale).
Caleth,

2
Se hai in mente un assembly specifico, allora usalo e non scrivere C non complicato. Quindi tutti sapranno che stai usando uno specifico trucco non portatile. E non sono le persone cattive che significano che non puoi usare UB, è Intel ecc. Che fa i suoi trucchi su chip.
Caleth,

2
@ 500-InternalServerError perché potrebbero non essere facilmente rilevabili o potrebbero non essere rilevabili affatto nel caso generale e quindi non ci sarebbe modo di impedirli. Che è diverso dalle violazioni della grammatica che possono essere rilevate. Inoltre, non è richiesta alcuna diagnosi mal formata e mal formata che in generale separa i programmi mal formati che potrebbero essere rilevati in teoria da quelli che in teoria non potrebbero essere rilevati in modo affidabile.
Shafik Yaghmour,

164

No, è terribile.

Il comportamento dell'uso di una variabile non inizializzata non è definito in C e C ++ ed è molto improbabile che un tale schema abbia proprietà statistiche desiderabili.

Se vuoi un generatore di numeri casuali "veloce e sporco", allora rand()è la soluzione migliore. Nella sua implementazione, tutto ciò che fa è una moltiplicazione, un'aggiunta e un modulo.

Il generatore più veloce che conosco richiede di utilizzare a uint32_tcome tipo di variabile pseudo-casuale Ie utilizzare

I = 1664525 * I + 1013904223

per generare valori successivi. Puoi scegliere qualsiasi valore iniziale di I(chiamato seme ) che ti piace. Ovviamente puoi codificarlo in linea. Il wraparound standard garantito di un tipo senza segno funge da modulo. (Le costanti numeriche sono raccolte a mano da quel notevole programmatore scientifico Donald Knuth.)


9
Il generatore "congruenziale lineare" presentato è utile per applicazioni semplici, ma solo per applicazioni non crittografiche. È possibile prevederne il comportamento. Vedi ad esempio " Decifrare una crittografia congruenziale lineare " dello stesso Don Knuth (Transazioni sulla teoria dell'informazione, IEEE Volume 31)
Jay

24
@Jay rispetto a una variabile unitaria per veloce e sporco? Questa è una soluzione molto migliore.
Mike McMahon,

2
rand()non è adatto allo scopo e dovrebbe essere completamente deprecato, secondo me. In questi giorni è possibile scaricare generatori di numeri casuali con licenza gratuita e di gran lunga superiori (ad esempio Mersenne Twister) che sono quasi altrettanto veloci con la massima facilità, quindi non c'è davvero bisogno di continuare a utilizzare l'alta difettosarand()
Jack Aidley

1
rand () ha un altro problema terribile: usa una sorta di blocco, chiamato all'interno dei thread che rallenta drasticamente il codice. Almeno, c'è una versione rientrante. E se usi C ++ 11, l'API casuale fornisce tutto ciò di cui hai bisogno.
Marwan Burelle,

4
Ad essere sinceri non ha chiesto se fosse un buon generatore di numeri casuali. Ha chiesto se fosse veloce. Beh, sì, probabilmente è il digiuno., Ma i risultati non saranno affatto casuali.
jcoder,

42

Buona domanda!

Non definito non significa che sia casuale. Pensaci, i valori che otterrai nelle variabili globali non inizializzate sarebbero stati lasciati lì dal sistema o dalle tue / altre applicazioni in esecuzione. A seconda di ciò che il sistema fa con la memoria non più utilizzata e / o del tipo di valori generati dal sistema e dalle applicazioni, è possibile ottenere:

  1. Sempre la stessa.
  2. Essere uno di un piccolo insieme di valori.
  3. Ottieni valori in uno o più intervalli piccoli.
  4. Vedi molti valori divisibili per 2/4/8 dai puntatori sul sistema 16/32/64 bit
  5. ...

I valori che otterrai dipenderanno completamente dai valori non casuali lasciati dal sistema e / o dalle applicazioni. Quindi, in effetti ci sarà un po 'di rumore (a meno che il tuo sistema non cancelli più la memoria), ma il pool di valori da cui attingerai non sarà affatto casuale.

Le cose peggiorano molto per le variabili locali perché provengono direttamente dallo stack del tuo programma. È molto probabile che il programma scriva effettivamente queste posizioni dello stack durante l'esecuzione di altro codice. Stimo le possibilità di fortuna in questa situazione molto bassa e un cambio di codice "casuale" che fai prova questa fortuna.

Leggi di casualità . Come vedrai la casualità è una proprietà molto specifica e difficile da ottenere. È un errore comune pensare che se prendi qualcosa che è difficile da tracciare (come il tuo suggerimento) otterrai un valore casuale.


7
... e questo tralasciando tutte le ottimizzazioni del compilatore che eliminerebbero completamente quel codice.
Deduplicatore

6 ... Otterrai una "casualità" diversa in Debug e Release. Non definito significa che stai sbagliando.
Sql Surfer,

Destra. Vorrei abbreviare o riassumere con "undefined"! = "Arbitrary"! = "Random". Tutti questi tipi di "incognita" hanno proprietà diverse.
fche il

È garantito che le variabili globali abbiano un valore definito, inizializzato o meno in modo esplicito. Questo è sicuramente vero in C ++ e in C pure .
Brian Vandenberg,

32

Molte buone risposte, ma mi permettono di aggiungere un altro e sottolineare il fatto che in un computer deterministico, nulla è casuale. Questo vale sia per i numeri prodotti da uno pseudo-RNG che per i numeri apparentemente "casuali" trovati in aree di memoria riservate alle variabili locali C / C ++ nello stack.

MA ... c'è una differenza cruciale.

I numeri generati da un buon generatore pseudocasuale hanno le proprietà che li rendono statisticamente simili ai disegni veramente casuali. Ad esempio, la distribuzione è uniforme. La durata del ciclo è lunga: puoi ottenere milioni di numeri casuali prima che il ciclo si ripeta. La sequenza non è correlata automaticamente: per esempio, non inizierai a vedere emergere strani schemi se prendi ogni 2 °, 3 ° o 27 ° numero o se guardi cifre specifiche nei numeri generati.

Al contrario, i numeri "casuali" lasciati nello stack non hanno nessuna di queste proprietà. I loro valori e la loro apparente casualità dipendono interamente da come viene costruito il programma, da come viene compilato e da come viene ottimizzato dal compilatore. A titolo di esempio, ecco una variante della tua idea come programma autonomo:

#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 compilo questo codice con GCC su una macchina Linux ed eseguo, risulta essere deterministico piuttosto spiacevole:

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 guardassi il codice compilato con un disassemblatore, potresti ricostruire in dettaglio ciò che stava succedendo. La prima chiamata a notrandom () utilizzava un'area dello stack che non era stata precedentemente utilizzata da questo programma; chissà cosa c'era dentro. Ma dopo quella chiamata a notrandom (), c'è una chiamata a printf () (che il compilatore GCC effettivamente ottimizza su una chiamata a putchar (), ma non importa) e che sovrascrive lo stack. Quindi, le volte successiva e successiva, quando viene chiamato notrandom (), lo stack conterrà dati non aggiornati dall'esecuzione di putchar () e poiché putchar () viene sempre chiamato con gli stessi argomenti, questi dati non aggiornati saranno sempre gli stessi, pure.

Quindi non c'è assolutamente nulla di casuale in questo comportamento, né i numeri ottenuti in questo modo hanno le proprietà desiderabili di un generatore di numeri pseudocasuali ben scritto. In effetti, nella maggior parte degli scenari della vita reale, i loro valori saranno ripetitivi e altamente correlati.

In effetti, come altri, prenderei seriamente in considerazione l'idea di licenziare qualcuno che ha cercato di far passare questa idea come un "RNG ad alte prestazioni".


1
"In un computer deterministico, nulla è casuale" - Questo non è in realtà vero. I computer moderni contengono tutti i tipi di sensori che consentono di produrre casualità reali e imprevedibili senza generatori hardware separati. Su un'architettura moderna, i valori di /dev/randomsono spesso derivati ​​da tali fonti hardware e sono in realtà "rumore quantico", cioè veramente imprevedibile nel miglior senso fisico della parola.
Konrad Rudolph,

2
Ma allora, non è un computer deterministico, vero? Ora fai affidamento su input ambientali. In ogni caso, questo ci porta ben oltre la discussione di un pseudo-RNG convenzionale rispetto a bit "casuali" nella memoria non inizializzata. Inoltre ... guarda la descrizione di / dev / random per apprezzare fino a che punto gli attuatori hanno fatto di tutto per assicurarsi che i numeri casuali siano crittograficamente sicuri ... proprio perché le sorgenti di input non sono rumore quantico puro, non correlato ma piuttosto, letture del sensore potenzialmente altamente correlate con solo un piccolo grado di casualità. È anche abbastanza lento.
Viktor Toth,

29

Comportamento indefinito significa che gli autori di compilatori sono liberi di ignorare il problema perché i programmatori non avranno mai il diritto di lamentarsi di ciò che accade.

Mentre in teoria quando si entra in UB land tutto può succedere (incluso un demone che vola via dal naso ) ciò che normalmente significa è che gli autori del compilatore non se ne cureranno e, per le variabili locali, il valore sarà qualunque cosa sia nella memoria dello stack in quel punto .

Ciò significa anche che spesso il contenuto sarà "strano" ma fisso o leggermente casuale o variabile ma con un chiaro schema evidente (ad esempio valori crescenti ad ogni iterazione).

Di sicuro non puoi aspettarti che sia un generatore casuale decente.


28

Il comportamento indefinito non è definito. Ciò non significa che ottieni un valore indefinito, significa che il programma può fare qualsiasi cosa e comunque soddisfare le specifiche del linguaggio.

Un buon compilatore di ottimizzazione dovrebbe richiedere

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 compilarlo in un noop. Questo è sicuramente più veloce di qualsiasi alternativa. Ha il lato negativo che non farà nulla, ma tale è il lato negativo di un comportamento indefinito.


3
Molto dipende dal fatto che lo scopo di un compilatore sia aiutare i programmatori a produrre file eseguibili che soddisfano i requisiti del dominio o se lo scopo è produrre l'eseguibile più "efficiente" il cui comportamento sarà coerente con i requisiti minimi dello Standard C, senza valutare se tale comportamento servirà a qualsiasi scopo utile. Per quanto riguarda il primo obiettivo, fare in modo che il codice utilizzi alcuni valori iniziali arbitrari per r, g, b o attivare una trappola del debugger, se pratico, sarebbe più utile che trasformare il codice in un nop. Per quanto riguarda quest'ultimo obiettivo ...
supercat

2
... un compilatore ottimale dovrebbe determinare quali input causerebbero l'esecuzione del metodo sopra ed eliminare qualsiasi codice che sarebbe rilevante solo quando tali input fossero ricevuti.
supercat

1
@supercat O il suo scopo potrebbe essere C. di produrre eseguibili efficienti in conformità con lo Standard, aiutando il programmatore a trovare luoghi in cui la conformità potrebbe non essere utile. I compilatori possono raggiungere questo scopo di compromesso emettendo più diagnostica di quanto richiesto dallo standard, come GCC -Wall -Wextra.
Damian Yerrick,

1
Il fatto che i valori non siano definiti non significa che il comportamento del codice circostante non sia definito. Nessun compilatore dovrebbe Noop quella funzione. Le due chiamate di funzione, indipendentemente dagli input forniti, DEVONO assolutamente essere chiamate; il primo DEVE essere chiamato con tre numeri compresi tra 0 e 255 e il secondo DEVE essere chiamato con un valore vero o falso. Un "buon compilatore di ottimizzazione" potrebbe ottimizzare i parametri della funzione su valori statici arbitrari, eliminando completamente le variabili, ma questo è quanto possibile (beh, a meno che le funzioni stesse non possano essere ridotte a noops su determinati input).
Dewi Morgan,

@DewiMorgan - poiché le funzioni chiamate sono del tipo "imposta questo parametro", quasi certamente si riducono a noops quando l'input è lo stesso del valore corrente del parametro, che il compilatore è libero di assumere nel caso.
Jules il

18

Non ancora menzionato, ma i percorsi di codice che invocano comportamenti indefiniti sono autorizzati a fare qualsiasi cosa voglia il compilatore, ad es

void updateEffect(){}

Che è certamente più veloce del tuo loop corretto, e grazie a UB, è perfettamente conforme.


18

Per motivi di sicurezza, è necessario pulire la nuova memoria assegnata a un programma, altrimenti le informazioni potrebbero essere utilizzate e le password potrebbero passare da un'applicazione all'altra. Solo quando si riutilizza la memoria, si ottengono valori diversi da 0. Ed è molto probabile che su uno stack il valore precedente sia appena corretto, poiché è stato riparato l'uso precedente di quella memoria.


13

Il tuo particolare esempio di codice probabilmente non farebbe ciò che ti aspetti. Mentre tecnicamente ogni iterazione del ciclo ricrea le variabili locali per i valori r, g e b, in pratica è lo stesso spazio di memoria nello stack. Quindi non verrà randomizzato ad ogni iterazione e finirai per assegnare gli stessi 3 valori per ciascuno dei 1000 colori, indipendentemente da quanto casuali siano r, g e b individualmente e inizialmente.

In effetti, se funzionasse, sarei molto curioso di sapere cosa lo sta ri-randomizzando. L'unica cosa che mi viene in mente sarebbe un interrupt intercalato che era impacchettato in cima a quella pila, altamente improbabile. Forse l'ottimizzazione interna che manteneva quelle come variabili di registro piuttosto che come posizioni di memoria reali, in cui i registri vengono riutilizzati più in basso nel ciclo, farebbe anche il trucco, specialmente se la funzione di visibilità impostata è particolarmente affamata di registro. Comunque, tutt'altro che casuale.


12

Come la maggior parte delle persone qui ha menzionato un comportamento indefinito. Undefined significa anche che potresti ottenere un valore intero valido (per fortuna) e in questo caso sarà più veloce (poiché la chiamata alla funzione rand non viene effettuata). Ma non usarlo praticamente. Sono sicuro che questo avrà risultati terribili poiché la fortuna non è sempre con te.


1
Ottimo punto! Potrebbe essere un trucco pragmatico, ma in effetti uno che richiede fortuna.
significato-importa

1
Non c'è assolutamente nessuna fortuna. Se il compilatore non ottimizza il comportamento indefinito, i valori che otterrai saranno perfettamente deterministici (= dipendono interamente dal tuo programma, dai suoi input, dal suo compilatore, dalle librerie che usa, dai tempi dei suoi thread se ha thread). Il problema è che non è possibile ragionare su questi valori poiché dipendono dai dettagli di implementazione.
cmaster - ripristina monica il

In assenza di un sistema operativo con uno stack di gestione degli interrupt separato dallo stack dell'applicazione, la fortuna potrebbe anche essere coinvolta, poiché gli interrupt disturbano frequentemente il contenuto della memoria leggermente al di là del contenuto dello stack corrente.
supercat

12

Veramente male! Cattiva abitudine, cattivo risultato. Tener conto di:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

Se la funzione A_Function_that_use_a_lot_the_Stack()effettua sempre la stessa inizializzazione, lascia lo stack con gli stessi dati. Questi dati sono ciò che chiamiamo updateEffect(): sempre lo stesso valore! .


11

Ho eseguito un test molto semplice e non è stato affatto casuale.

#include <stdio.h>

int main() {

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

Ogni volta che ho eseguito il programma, ha stampato lo stesso numero ( 32767nel mio caso) - non puoi essere molto meno casuale di così. Questo è presumibilmente qualunque sia il codice di avvio nella libreria di runtime lasciato nello stack. Poiché utilizza lo stesso codice di avvio ogni volta che il programma viene eseguito e nient'altro varia nel programma tra le esecuzioni, i risultati sono perfettamente coerenti.


Buon punto. Un risultato dipende fortemente da dove viene chiamato questo generatore di numeri "casuale" nel codice. È piuttosto imprevedibile che casuale.
NO_NAME il

10

Devi avere una definizione di cosa intendi per "casuale". Una definizione ragionevole implica che i valori ottenuti dovrebbero avere poca correlazione. È qualcosa che puoi misurare. Inoltre, non è banale da ottenere in modo controllato e riproducibile. Quindi un comportamento indefinito non è certamente quello che stai cercando.


7

Ci sono alcune situazioni in cui la memoria non inizializzata può essere letta in modo sicuro usando il tipo "unsigned char *" [es. Un buffer restituito da malloc]. Il codice può leggere tale memoria senza doversi preoccupare del compilatore che lancia la causalità fuori dalla finestra e ci sono momenti in cui può essere più efficiente avere il codice preparato per tutto ciò che la memoria potrebbe contenere rispetto a garantire che i dati non inizializzati non vengano letti ( un esempio comune di questo sarebbe usarememcpy su buffer parzialmente inizializzato piuttosto che copiare discretamente tutti gli elementi che contengono dati significativi).

Anche in questi casi, tuttavia, si dovrebbe sempre supporre che se una qualsiasi combinazione di byte sarà particolarmente vessatoria, la lettura produrrà sempre quel modello di byte (e se un certo modello sarebbe vessatorio nella produzione, ma non nello sviluppo, tale il modello non verrà visualizzato fino a quando il codice non sarà in produzione).

La lettura della memoria non inizializzata potrebbe essere utile come parte di una strategia di generazione casuale in un sistema incorporato in cui si può essere certi che la memoria non sia mai stata scritta con contenuti sostanzialmente non casuali dall'ultima volta che il sistema è stato acceso e se la produzione il processo utilizzato per la memoria fa variare il suo stato di accensione in modo semi-casuale. Il codice dovrebbe funzionare anche se tutti i dispositivi generano sempre gli stessi dati, ma nei casi in cui ad esempio un gruppo di nodi ciascuno deve selezionare ID univoci arbitrari il più rapidamente possibile, avendo un generatore "non molto casuale" che dia alla metà dei nodi lo stesso iniziale L'identificazione potrebbe essere migliore di non avere alcuna fonte iniziale di casualità.


2
"se una qualsiasi combinazione di byte sarà particolarmente vessatoria, la sua lettura produrrà sempre quel modello di byte" - finché non codifichi per far fronte a quel modello, a quel punto non sarà più vessatorio e in futuro verrà letto un modello diverso.
Steve Jessop,

@SteveJessop: Precisamente. La mia linea sullo sviluppo rispetto alla produzione era destinata a trasmettere un'idea simile. Al codice non dovrebbe interessare ciò che è nella memoria non inizializzata al di là di una vaga nozione di "Qualche casualità potrebbe essere piacevole". Se il comportamento del programma è influenzato dal contenuto di un pezzo di memoria non inizializzata, il contenuto di pezzi acquisiti in futuro potrebbe a sua volta essere influenzato da quello.
supercat

5

Come altri hanno già detto, sarà veloce, ma non casuale.

Quello che la maggior parte dei compilatori farà per le variabili locali è di prendere un po 'di spazio per loro nello stack, ma non preoccuparsi di impostarlo su nulla (lo standard dice che non è necessario, quindi perché rallentare il codice che stai generando?).

In questo caso, il valore che otterrai dipenderà da ciò che era precedentemente nello stack - se chiami una funzione prima di questa che ha un centinaio di variabili char locali tutte impostate su 'Q' e poi chiami che sei la funzione dopo che ritorna, quindi probabilmente troverai che i tuoi valori "casuali" si comportano come se memset()li avessi tutti su "Q".

È importante sottolineare che per la tua funzione di esempio che cerca di usare questo, questi valori non cambieranno ogni volta che li leggi, saranno sempre gli stessi. Quindi otterrai 100 stelle tutte impostate sullo stesso colore e visibilità.

Inoltre, nulla dice che il compilatore non dovrebbe inizializzare questi valori, quindi un compilatore futuro potrebbe farlo.

In generale: cattiva idea, non farlo. (come molte ottimizzazioni a livello di codice "intelligenti" davvero ...)


2
Stai facendo alcune forti previsioni su ciò che accadrà, sebbene nulla di tutto ciò sia garantito a causa di UB. Inoltre, non è vero in pratica.
usr

3

Come altri hanno già detto, si tratta di un comportamento indefinito ( UB ), ma potrebbe "funzionare".

Tranne i problemi già menzionati da altri, vedo un altro problema (svantaggio): non funzionerà in nessun linguaggio diverso da C e C ++. So che questa domanda riguarda C ++, ma se riesci a scrivere codice che sarà un buon codice C ++ e Java e non è un problema, perché no? Forse un giorno qualcuno dovrà portarlo in un'altra lingua e cercare bug causati da "trucchi magici" UB come questo sarà sicuramente un incubo (specialmente per uno sviluppatore C / C ++ inesperto).

Qui c'è una domanda su un altro UB simile. Immagina di provare a trovare un bug come questo senza conoscere questo UB. Se vuoi leggere di più su cose così strane in C / C ++, leggi le risposte alle domande dal link e guarda questa GRANDE presentazione. Ti aiuterà a capire cosa c'è sotto il cofano e come funziona; non è solo un'altra presentazione piena di "magia". Sono abbastanza sicuro che anche la maggior parte dei programmatori C / c ++ esperti può imparare molto da questo.


3

Non è una buona idea affidare la nostra logica al comportamento indefinito del linguaggio. Oltre a quanto menzionato / discusso in questo post, vorrei menzionare che con il moderno approccio / stile C ++ tale programma potrebbe non essere compilabile.

Questo è stato menzionato nel mio precedente post che contiene il vantaggio della funzione automatica e del link utile per lo stesso.

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

Quindi, se cambiamo il codice sopra e sostituiamo i tipi effettivi con auto , il programma non verrebbe nemmeno compilato.

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);
    }
}

3

Mi piace il tuo modo di pensare. Davvero fuori dagli schemi. Tuttavia, il compromesso non vale davvero la pena. Il compromesso del runtime della memoria è una cosa, incluso il comportamento indefinito per il runtime non lo è .

Deve darti una sensazione molto inquietante sapere che stai usando una logica "casuale" come la tua logica aziendale. Non lo farei.


3

Usa 7757ogni luogo in cui sei tentato di usare variabili non inizializzate. L'ho scelto a caso da un elenco di numeri primi:

  1. è un comportamento definito

  2. è garantito che non sia sempre 0

  3. è primo

  4. è probabile che sia statisticamente casuale come variabili non inizializzate

  5. è probabile che sia più veloce delle variabili non inizializzate poiché il suo valore è noto al momento della compilazione


Per un confronto, vedere i risultati in questa risposta: stackoverflow.com/a/31836461/2963099
Glenn Teitelbaum,

1

C'è un'altra possibilità da considerare.

I compilatori moderni (ahem g ++) sono così intelligenti che esaminano il tuo codice per vedere quali istruzioni influenzano lo stato e cosa no e se un'istruzione NON influisce sullo stato, g ++ rimuoverà semplicemente quell'istruzione.

Quindi ecco cosa accadrà. g ++ vedrà sicuramente che stai leggendo, eseguendo l'aritmetica, risparmiando, che è essenzialmente un valore di immondizia, che produce più immondizia. Dal momento che non vi è alcuna garanzia che la nuova immondizia sia più utile di quella vecchia, semplicemente eliminerà il ciclo. BLOOP!

Questo metodo è utile, ma ecco cosa farei. Combina UB (Undefined Behaviour) con velocità rand ().

Naturalmente, riduci le rand()s eseguite, ma mescolale in modo che il compilatore non faccia nulla che tu non voglia.

E non ti licenzierò.


Trovo molto difficile credere che un compilatore possa decidere che il tuo codice sta facendo qualcosa di stupido e rimuoverlo. Mi aspetterei solo di ottimizzare il codice inutilizzato , non il codice non consigliabile . Hai un caso di test riproducibile? Ad ogni modo, la raccomandazione di UB è pericolosa. Inoltre, GCC non è l'unico compilatore competente in circolazione, quindi non è giusto definirlo "moderno".
underscore_d

-1

L'uso di dati non inizializzati per casualità non è necessariamente una cosa negativa se fatto correttamente. In effetti, OpenSSL fa esattamente questo per seminare il suo PRNG.

Apparentemente, tuttavia, questo utilizzo non è stato ben documentato, perché qualcuno ha notato Valgrind lamentarsi dell'utilizzo di dati non inizializzati e "risolto", causando un errore nel PRNG .

Quindi puoi farlo, ma devi sapere cosa stai facendo e assicurarti che chiunque legga il tuo codice lo capisca.


1
Questo dipenderà dal tuo compilatore, che è previsto con un comportamento indefinito, come possiamo vedere dalla mia risposta, il clang di oggi non farà ciò che vogliono.
Shafik Yaghmour,

6
Il fatto che OpenSSL abbia usato questo metodo come input entropico non dice che fosse positivo. Dopotutto, l'unica altra fonte di entropia che hanno usato era il PID . Non esattamente un buon valore casuale. Da qualcuno che si affida a una fonte di entropia così cattiva, non mi aspetto un buon giudizio sull'altra fonte di entropia. Spero solo che le persone che attualmente mantengono OpenSSL siano più brillanti.
cmaster - ripristina monica il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.