Wiem, że niezainicjowana zmienna lokalna jest nieokreślonym zachowaniem ( UB ), a także wartość może mieć reprezentacje pułapek, które mogą wpływać na dalsze działanie, ale czasami chcę użyć liczby losowej tylko do reprezentacji wizualnej i nie będę jej dalej używać w innej części program, na przykład, ustaw coś z losowym kolorem w efekcie wizualnym, na przykład:
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);
}
}
czy to tak szybciej niż
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);
}
}
a także szybszy niż inny generator liczb losowych?
c++
c
undefined-behavior
garbage
grgrr
źródło
źródło
Odpowiedzi:
Jak zauważyli inni, jest to niezdefiniowane zachowanie (UB).
W praktyce będzie (prawdopodobnie) faktycznie działał. Odczyt z niezainicjowanego rejestru na architekturach x86 [-64] rzeczywiście spowoduje śmieciowe wyniki i prawdopodobnie nie zrobi nic złego (w przeciwieństwie do np. Itanium, gdzie rejestry mogą być oznaczone jako nieprawidłowe , dzięki czemu odczyty propagują błędy takie jak NaN).
Istnieją jednak dwa główne problemy:
To nie będzie szczególnie losowe. W tym przypadku czytasz ze stosu, więc dostaniesz wszystko, co było wcześniej. Które mogą być efektywnie losowe, całkowicie ustrukturyzowane, hasło, które wprowadziłeś dziesięć minut temu lub przepis na ciasteczka twojej babci.
Zła (duża „B”) praktyka pozwala wpuszczać takie rzeczy do twojego kodu. Technicznie kompilator może wstawiać za
reformat_hdd();
każdym razem, gdy czyta się niezdefiniowaną zmienną. Nie będzie , ale i tak nie powinieneś tego robić. Nie rób niebezpiecznych rzeczy. Im mniej robisz wyjątków, tym bardziej jesteś bezpieczny od przypadkowych błędów przez cały czas.Bardziej palącym problemem związanym z UB jest to, że powoduje, że zachowanie całego programu jest niezdefiniowane. Nowoczesne kompilatory mogą to wykorzystać, aby ominąć ogromne obszary kodu, a nawet cofnąć się w czasie . Zabawa z UB przypomina wiktoriańskiego inżyniera demontującego żywy reaktor jądrowy. Zillion ma wiele rzeczy do zrobienia i prawdopodobnie nie poznasz połowy podstawowych zasad lub wdrożonej technologii. To może być w porządku, ale nadal nie powinieneś na to pozwolić. Spójrz na inne fajne odpowiedzi, aby uzyskać szczegółowe informacje.
Również cię zwolnię.
źródło
unreachable()
i usunąć połowę programu. Zdarza się to również w praktyce. Takie zachowanie całkowicie zneutralizowało RNG w niektórych dystrybucjach Linuksa; Większość odpowiedzi w tym pytaniu wydaje się zakładać, że niezainicjowana wartość w ogóle zachowuje się jak wartość. To nieprawda.Powiem to jasno: w naszych programach nie odwołujemy się do nieokreślonego zachowania . To nigdy nie jest dobry pomysł, kropka. Istnieją rzadkie wyjątki od tej zasady; na przykład, jeśli jesteś implementatorem biblioteki implementującym offsetof . Jeśli Twoja sprawa objęta jest takim wyjątkiem, prawdopodobnie już o tym wiesz. W tym przypadku wiemy, że użycie niezainicjowanych zmiennych automatycznych jest zachowaniem niezdefiniowanym .
Kompilatory stały się bardzo agresywne dzięki optymalizacjom dotyczącym nieokreślonego zachowania i możemy znaleźć wiele przypadków, w których nieokreślone zachowanie doprowadziło do wad bezpieczeństwa. Najbardziej niesławnym przypadkiem jest prawdopodobnie usunięcie sprawdzania pustego wskaźnika jądra systemu Linux, o którym wspomniałem w mojej odpowiedzi na błąd kompilacji C ++? gdzie optymalizacja kompilatora wokół niezdefiniowanego zachowania zamieniła skończoną pętlę w nieskończoną.
Możemy przeczytać Niebezpieczne optymalizacje CERT i utratę przyczynowości ( wideo ), które mówią między innymi:
W szczególności w odniesieniu do nieokreślonych wartości, raport defektu standardowego C 451: Niestabilność niezainicjowanych zmiennych automatycznych stanowi ciekawy odczyt. Nie został jeszcze rozwiązany, ale wprowadza pojęcie niestabilnych wartości, co oznacza, że nieokreśloność wartości może rozprzestrzeniać się w programie i może mieć różne nieokreślone wartości w różnych punktach programu.
Nie znam żadnych przykładów, w których tak się dzieje, ale w tym momencie nie możemy tego wykluczyć.
Prawdziwe przykłady, a nie oczekiwany wynik
Jest mało prawdopodobne, aby uzyskać losowe wartości. Kompilator może całkowicie zoptymalizować pętlę odejścia. Na przykład w tym uproszczonym przypadku:
clang optymalizuje go ( zobacz na żywo ):
lub może wszystkie zera, jak w tym zmodyfikowanym przypadku:
zobacz na żywo :
Oba te przypadki są całkowicie akceptowalnymi formami niezdefiniowanego zachowania.
Uwaga: jeśli jesteśmy na Itanium, możemy otrzymać wartość pułapki :
Inne ważne uwagi
Interesujące jest odnotowanie różnicy między gcc i clang, odnotowanej w projekcie Kanarek UB, nad tym, jak chętnie wykorzystują niezdefiniowane zachowanie w odniesieniu do niezainicjowanej pamięci. Artykuł zauważa ( moje podkreślenie ):
Jak zauważa Matthieu M. Co każdy programista C powinien wiedzieć o nieokreślonym zachowaniu # 2/3, jest również istotny dla tego pytania. Mówi między innymi ( moje podkreślenie ):
Dla kompletności powinienem chyba wspomnieć, że implementacje mogą zdecydować o tym, aby niezdefiniowane zachowanie było dobrze zdefiniowane, na przykład gcc pozwala na pisanie przez związki, podczas gdy w C ++ wydaje się to niezdefiniowanym zachowaniem . W takim przypadku wdrożenie powinno to udokumentować i zwykle nie będzie to przenośne.
źródło
Nie, to okropne.
Zachowanie używania niezainicjowanej zmiennej jest niezdefiniowane zarówno w C, jak i C ++, i jest bardzo mało prawdopodobne, aby taki schemat miał pożądane właściwości statystyczne.
Jeśli chcesz „szybkiego i brudnego” generatora liczb losowych, to
rand()
jest najlepszy wybór. W swojej implementacji wszystko, co robi, to mnożenie, dodawanie i moduł.Najszybszy generator, jaki znam, wymaga użycia
uint32_t
jako typu pseudolosowej zmiennejI
i użyciaI = 1664525 * I + 1013904223
generować kolejne wartości. Możesz wybrać dowolną wartość początkową
I
(zwaną ziarnem ), która ci się spodoba. Oczywiście możesz kodować to wstawianie. Standardowo gwarantowane objęcie typu bez znaku działa jako moduł. (Stałe numeryczne są wybierane ręcznie przez tego niezwykłego programistę naukowego Donalda Knutha.)źródło
rand()
nie nadaje się do celu i moim zdaniem powinien być całkowicie przestarzały. W dzisiejszych czasach możesz pobrać swobodnie licencjonowane i znacznie lepsze generatory liczb losowych (np. Mersenne Twister), które są prawie tak szybkie i bardzo łatwe, więc naprawdę nie ma potrzeby dalszego używania wysoce wadliwegorand()
Dobre pytanie!
Niezdefiniowany nie oznacza, że jest losowy. Pomyśl o tym, wartości, które uzyskasz w globalnych niezainicjowanych zmiennych, zostały tam pozostawione przez system lub działające aplikacje. W zależności od tego, co robi Twój system z nieużywaną pamięcią i / lub jakie wartości generuje system i aplikacje, możesz uzyskać:
Wartości, które otrzymasz, całkowicie zależą od tego, które nieprzypadkowe wartości pozostawiają system i / lub aplikacje. Tak więc rzeczywiście będzie trochę szumu (chyba że twój system wyczyści pamięć, która nie jest już używana), ale pula wartości, z której będziesz czerpać, nie będzie losowa.
W przypadku zmiennych lokalnych sytuacja staje się znacznie gorsza, ponieważ pochodzą one bezpośrednio ze stosu własnego programu. Istnieje bardzo duża szansa, że Twój program zapisze te lokalizacje stosu podczas wykonywania innego kodu. Szanse na szczęście w tej sytuacji oceniam na bardzo niskie, a dokonana przez ciebie „losowa” zmiana kodu próbuje tego szczęścia.
Przeczytaj o losowości . Jak zobaczysz, losowość jest bardzo specyficzną i trudną do uzyskania własnością. Powszechnym błędem jest myślenie, że jeśli weźmiesz coś trudnego do śledzenia (np. Twoją sugestię), otrzymasz losową wartość.
źródło
Wiele dobrych odpowiedzi, ale pozwolę sobie dodać jeszcze jedną i podkreślić, że w deterministycznym komputerze nic nie jest przypadkowe. Dotyczy to zarówno liczb generowanych przez pseudo-RNG, jak i pozornie „losowych” liczb znajdujących się w obszarach pamięci zarezerwowanych dla zmiennych lokalnych C / C ++ na stosie.
ALE ... jest zasadnicza różnica.
Liczby generowane przez dobry generator pseudolosowy mają właściwości, które czynią je statystycznie podobnymi do losowych losowań. Na przykład rozkład jest jednolity. Długość cyklu jest długa: można uzyskać miliony liczb losowych, zanim cykl się powtórzy. Sekwencja nie jest autokorelowana: na przykład nie zaczniesz pojawiać się dziwnych wzorów, jeśli weźmiesz co 2, 3 lub 27 liczbę lub spojrzysz na konkretne cyfry w generowanych liczbach.
Natomiast „losowe” liczby pozostawione na stosie nie mają żadnej z tych właściwości. Ich wartości i ich pozorna losowość zależą całkowicie od tego, jak program jest zbudowany, jak jest kompilowany i jak jest optymalizowany przez kompilator. Przykładowo, oto odmiana twojego pomysłu jako samodzielnego programu:
Kiedy kompiluję ten kod za pomocą GCC na komputerze z systemem Linux i okazuje się, że jest to raczej nieprzyjemnie deterministyczne:
Jeśli spojrzysz na skompilowany kod za pomocą dezasemblera, możesz szczegółowo zrekonstruować, co się dzieje. Pierwsze wywołanie notrandom () wykorzystało obszar stosu, który wcześniej nie był używany przez ten program; kto wie co tam było. Ale po tym wywołaniu notrandom () następuje wywołanie printf () (które kompilator GCC faktycznie optymalizuje do wywołania putchar (), ale nieważne), co zastępuje stos. Tak więc następnym razem i po wywołaniu notrandom () stos będzie zawierał nieaktualne dane z wykonania putchar (), a ponieważ putchar () jest zawsze wywoływany z tymi samymi argumentami, te nieaktualne dane zawsze będą takie same, też.
Tak więc nie ma absolutnie nic losowego w tym zachowaniu, ani liczby uzyskane w ten sposób nie mają żadnych pożądanych właściwości dobrze napisanego generatora liczb pseudolosowych. W rzeczywistości w większości rzeczywistych scenariuszy ich wartości będą powtarzalne i wysoce skorelowane.
Rzeczywiście, podobnie jak inni, poważnie rozważyłbym również zwolnienie kogoś, kto próbował przekazać ten pomysł jako „wysokowydajny RNG”.
źródło
/dev/random
często pochodzą z takich źródeł sprzętowych i są w rzeczywistości „szumem kwantowym”, tj. Naprawdę nieprzewidywalnym w najlepszym fizycznym tego słowa znaczeniu.Niezdefiniowane zachowanie oznacza, że autorzy kompilatorów mogą zignorować problem, ponieważ programiści nigdy nie będą mieli prawa narzekać na cokolwiek się stanie.
Podczas gdy teoretycznie przy wchodzeniu na ląd UB wszystko może się zdarzyć (w tym demon lecący z twojego nosa ), co zwykle oznacza, że autorzy kompilatora po prostu nie będą się tym przejmować, a dla zmiennych lokalnych wartością będzie cokolwiek, co jest w pamięci stosu w tym momencie .
Oznacza to również, że często treść będzie „dziwna”, ale stała lub nieco losowa lub zmienna, ale z wyraźnym wyraźnym wzorem (np. Rosnąca wartość przy każdej iteracji).
Na pewno nie można oczekiwać, że będzie to przyzwoity generator losowy.
źródło
Niezdefiniowane zachowanie jest niezdefiniowane. Nie oznacza to, że otrzymujesz niezdefiniowaną wartość, oznacza to, że program może zrobić wszystko i nadal spełniać specyfikację języka.
Dobry kompilator optymalizacyjny powinien wziąć
i skompiluj to do noop. Jest to z pewnością szybsze niż jakakolwiek alternatywa. Ma tę wadę, że nic nie zrobi, ale taka jest wada niezdefiniowanego zachowania.
źródło
-Wall -Wextra
.Nie wspomniano jeszcze, ale ścieżki kodu, które wywołują niezdefiniowane zachowanie, mogą wykonywać wszystko, co chce kompilator, np
Co jest z pewnością szybsze niż twoja właściwa pętla, a dzięki UB jest całkowicie zgodne.
źródło
Ze względów bezpieczeństwa nowa pamięć przypisana do programu musi zostać wyczyszczona, w przeciwnym razie informacje mogłyby zostać wykorzystane, a hasła mogłyby wyciekać z jednej aplikacji do drugiej. Dopiero po ponownym użyciu pamięci otrzymujesz wartości inne niż 0. I jest bardzo prawdopodobne, że na stosie poprzednia wartość została właśnie naprawiona, ponieważ poprzednie użycie tej pamięci zostało naprawione.
źródło
Twój przykładowy kod prawdopodobnie nie zrobiłby tego, czego oczekujesz. Podczas gdy technicznie każda iteracja pętli odtwarza zmienne lokalne dla wartości r, gib, w praktyce jest to dokładnie ta sama przestrzeń pamięci na stosie. W związku z tym nie będzie ponownie losowo przydzielany przy każdej iteracji, a w końcu przypisujesz te same 3 wartości dla każdego z 1000 kolorów, niezależnie od tego, jak losowo r, gib są indywidualnie i początkowo.
Rzeczywiście, gdyby zadziałało, byłbym bardzo ciekawy, co go ponownie randomizuje. Jedyne, co mogę wymyślić, to przeplatane przerwanie, które warkocze na szczycie stosu, bardzo mało prawdopodobne. Być może wewnętrzna optymalizacja, która zachowywała te zmienne jako zmienne rejestru, a nie jako prawdziwe miejsce w pamięci, gdzie rejestry są ponownie wykorzystywane w dalszej części pętli, również by załatwiła sprawę, szczególnie jeśli ustawiona funkcja widoczności jest szczególnie wymagająca rejestru. Nadal dalekie od losowości.
źródło
Jak większość osób wspomniała tutaj o niezdefiniowanym zachowaniu. Niezdefiniowana oznacza również, że możesz uzyskać pewną prawidłową wartość całkowitą (na szczęście), w tym przypadku będzie to szybsze (ponieważ nie jest wywoływane funkcja rand). Ale nie używaj go praktycznie. Jestem pewien, że przyniesie to okropne wyniki, ponieważ szczęście nie jest przez cały czas z tobą.
źródło
Naprawdę źle! Zły nawyk, zły wynik. Rozważać:
Jeśli funkcja
A_Function_that_use_a_lot_the_Stack()
zawsze wykonuje tę samą inicjalizację, pozostawia stos z tymi samymi danymi. Te dane nazywamyupdateEffect()
: zawsze ta sama wartość! .źródło
Przeprowadziłem bardzo prosty test i wcale nie był przypadkowy.
Za każdym razem, gdy uruchamiałem program, drukował ten sam numer (
32767
w moim przypadku) - nie można dostać dużo mniej losowego niż ten. Jest to prawdopodobnie dowolny kod startowy z biblioteki wykonawczej pozostawionej na stosie. Ponieważ używa tego samego kodu startowego przy każdym uruchomieniu programu i nic innego nie zmienia się w programie między uruchomieniami, wyniki są idealnie spójne.źródło
Musisz zdefiniować, co rozumiesz przez „losowy”. Rozsądna definicja zakłada, że otrzymywane wartości powinny mieć niewielką korelację. To jest coś, co możesz zmierzyć. Osiągnięcie w kontrolowany, powtarzalny sposób również nie jest łatwe. Tak więc nieokreślone zachowanie z pewnością nie jest tym, czego szukasz.
źródło
Istnieją pewne sytuacje, w których niezainicjowana pamięć może być bezpiecznie odczytana przy użyciu typu „unsigned char *” [np. Bufor zwrócony z
malloc
]. Kod może odczytywać taką pamięć, nie martwiąc się o to, że kompilator wyrzuci przyczynę przez okno. Czasami bardziej efektywne może być przygotowanie kodu na wszystko, co może zawierać pamięć, niż zapewnienie, że niezainicjowane dane nie zostaną odczytane ( powszechnym przykładem tego byłoby użyciememcpy
w częściowo zainicjowanym buforze zamiast dyskretnego kopiowania wszystkich elementów zawierających znaczące dane).Jednak nawet w takich przypadkach należy zawsze zakładać, że jeśli jakakolwiek kombinacja bajtów będzie szczególnie dokuczliwa, czytanie jej zawsze da taki wzór bajtów (a jeśli pewien wzorzec byłby dokuczliwy w produkcji, ale nie w rozwoju, taki wzorzec nie pojawi się, dopóki kod nie zostanie wyprodukowany).
Czytanie niezainicjowanej pamięci może być przydatne jako część strategii generowania losowego w systemie wbudowanym, w którym można mieć pewność, że pamięć nigdy nie została napisana z zasadniczo nieprzypadkową zawartością od czasu ostatniego włączenia systemu, a także proces zastosowany dla pamięci powoduje, że jej stan włączenia zmienia się w sposób pół-losowy. Kod powinien działać, nawet jeśli wszystkie urządzenia zawsze dają te same dane, ale w przypadkach, gdy np. Każda grupa węzłów musi jak najszybciej wybrać dowolne unikalne identyfikatory, mając generator „niezbyt losowy”, który daje połowie węzłów taki sam początkowy Identyfikator może być lepszy niż brak początkowego źródła losowości.
źródło
Jak powiedzieli inni, będzie szybki, ale nie losowy.
Większość kompilatorów zrobi dla zmiennych lokalnych, aby zdobyć dla nich miejsce na stosie, ale nie zawracać sobie głowy ustawieniem go na cokolwiek (standard mówi, że nie muszą, więc po co spowalniać generowanie kodu?).
W takim przypadku wartość, którą otrzymasz, będzie zależeć od tego, co było wcześniej na stosie - jeśli wywołasz funkcję przed tą, która ma sto lokalnych zmiennych char ustawionych na „Q”, a następnie wywołujesz funkcję po który zwraca, wtedy prawdopodobnie zauważysz, że twoje „losowe” wartości zachowują się tak, jakbyś miał
memset()
wszystkie na „Q”.Co ważne, dla przykładowej funkcji próbującej tego użyć, wartości te nie zmieniają się za każdym razem, gdy je czytasz, będą za każdym razem takie same. Otrzymasz 100 gwiazdek ustawionych w tym samym kolorze i widoczności.
Ponadto nic nie mówi, że kompilator nie powinien inicjować tych wartości - może to zrobić przyszły kompilator.
Ogólnie: zły pomysł, nie rób tego. (tak jak wiele „sprytnych” optymalizacji poziomu kodu naprawdę ...)
źródło
Jak już wspomniano inni, jest to zachowanie niezdefiniowane ( UB ), ale może „działać”.
Oprócz problemów, o których wspominali już inni, widzę jeszcze jeden problem (wadę) - nie będzie działał w żadnym języku innym niż C i C ++. Wiem, że to pytanie dotyczy C ++, ale jeśli możesz napisać kod, który będzie dobrym kodem C ++ i Java i nie jest to problem, to dlaczego nie? Być może pewnego dnia ktoś będzie musiał przenieść go na inny język i poszukiwanie błędów spowodowanych przez
„magiczne sztuczki”UB takie jak ten na pewno będzie koszmarem (szczególnie dla niedoświadczonego programisty C / C ++).Tutaj jest pytanie o inny podobny UB. Wyobraź sobie, że próbujesz znaleźć taki błąd, nie wiedząc o tym UB. Jeśli chcesz przeczytać więcej o takich dziwnych rzeczach w C / C ++, przeczytaj odpowiedzi na pytanie z linku i zobacz ten WIELKI pokaz slajdów. Pomoże ci zrozumieć, co jest pod maską i jak działa; to nie tylko kolejny pokaz pełen „magii”. Jestem pewien, że nawet większość doświadczonych programistów C / c ++ może się wiele z tego nauczyć.
źródło
Nie jest dobrym pomysłem opieranie naszej logiki na niezdefiniowanych zachowaniach językowych. Oprócz wszystkiego, co wspomniano / omówiono w tym poście, chciałbym wspomnieć, że przy nowoczesnym podejściu / stylu C ++ taki program może nie być kompilowany.
Zostało to wspomniane w moim poprzednim poście, który zawiera zaletę funkcji auto i przydatny link do tego samego.
https://stackoverflow.com/a/26170069/2724703
Jeśli więc zmienimy powyższy kod i zastąpimy rzeczywiste typy auto , program nawet się nie skompiluje.
źródło
Podoba mi się twój sposób myślenia. Naprawdę nieszablonowe. Jednak kompromis naprawdę nie jest tego wart. Pamięć-runtime kompromis jest rzeczą, w tym zachowanie niezdefiniowane na starcie jest nie .
To musi dać ci bardzo niepokojące uczucie, wiedząc, że używasz takiej „losowej” jak logika biznesowa. Nie zrobię tego.
źródło
Używaj
7757
każdego miejsca, w którym masz ochotę używać niezainicjowanych zmiennych. Wybrałem go losowo z listy liczb pierwszych:jest to określone zachowanie
gwarantuje się, że nie zawsze będzie to 0
to jest liczba pierwsza
prawdopodobnie będzie tak samo statystycznie losowy jak niezainicjowane zmienne
prawdopodobnie będzie szybszy niż niezainicjowane zmienne, ponieważ jego wartość jest znana w czasie kompilacji
źródło
Jest jeszcze jedna możliwość do rozważenia.
Nowoczesne kompilatory (ahem g ++) są tak inteligentne, że przechodzą przez twój kod, aby zobaczyć, które instrukcje wpływają na stan, a co nie, a jeśli gwarantowana instrukcja NIE wpłynie na stan, g ++ po prostu usunie tę instrukcję.
Oto co się stanie. g ++ na pewno zobaczy, że czytasz, wykonujesz arytmetykę, oszczędzasz, co jest w zasadzie wartością śmieci, która powoduje więcej śmieci. Ponieważ nie ma gwarancji, że nowe śmieci będą bardziej przydatne niż stare, po prostu usunie pętlę. BLOOP!
Ta metoda jest przydatna, ale oto, co bym zrobił. Połącz UB (niezdefiniowane zachowanie) z prędkością rand ().
Oczywiście zmniejsz
rand()
s wykonywane, ale wklej je, aby kompilator nie zrobił nic, czego nie chcesz.I nie zwolnię cię.
źródło
Używanie niezainicjowanych danych dla przypadkowości niekoniecznie jest złą rzeczą, jeśli jest wykonane właściwie. W rzeczywistości OpenSSL robi to dokładnie, aby zaszczepić swój PRNG.
Najwyraźniej to użycie nie było jednak dobrze udokumentowane, ponieważ ktoś zauważył, że Valgrind narzeka na używanie niezainicjowanych danych i „naprawił” je, powodując błąd w PRNG .
Możesz to zrobić, ale musisz wiedzieć, co robisz i upewnić się, że każdy czytający Twój kod to rozumie.
źródło