Чи неініціалізована локальна змінна є найшвидшим генератором випадкових чисел?


329

Я знаю, що неініціалізована локальна змінна - це невизначена поведінка ( UB ), а також значення може мати уявлення про пастку, що може вплинути на подальшу роботу, але іноді я хочу використовувати випадкове число лише для візуального подання і не буду надалі використовувати їх в іншій частині наприклад, встановіть щось із випадковим кольором у візуальному ефекті, наприклад:

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

чи швидше, ніж

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

а також швидше, ніж інший генератор випадкових чисел?


88
+1 Це абсолютно законне питання. Це правда, що на практиці неініціалізовані значення можуть бути випадковими. Справа в тому , що вони не особливо , і що це UB робить питати , що погано.
imallett

35
@imallett: Абсолютно. Це гарне запитання, і щонайменше одна стара гра Z80 (Amstrad / ZX Spectrum) у минулі часи використовувала свою програму як дані для встановлення своєї місцевості. Тож є навіть прецеденти. Не можу цього зробити сьогодні. Сучасні операційні системи забирають усе задоволення.
Вірсавія

81
Безумовно, головна проблема полягає в тому, що вона не є випадковою.
Джон

30
Насправді є приклад неініціалізованої змінної, яка використовується як випадкове значення, див. Катастрофу Debian RNG (приклад 4 у цій статті ).
PaperBirdMaster

31
На практиці - і повірте, я роблю багато налагоджень у різних архітектурах - ваше рішення може зробити дві речі: читання неініціалізованих регістрів, або неініціалізована пам'ять. Тепер, хоча "неініціалізований" означає певний спосіб випадковий, на практиці він, швидше за все, буде містити а) нулі , б) повторювані або послідовні значення (у випадку читання пам'яті, раніше зайняті цифровими носіями) або в) послідовне сміття з обмеженим значенням набір (у випадку зчитування пам'яті, яка раніше була зайнята кодованими цифровими даними). Жодне з них не є реальними джерелами ентропії.
mg30rg

Відповіді:


299

Як зазначали інші, це не визначена поведінка (UB).

На практиці це (мабуть) фактично (вид) спрацює. Читання з неініціалізованого реєстру на x86 [-64] архітектурах дійсно дасть результати сміття, і, ймовірно, не зробить нічого поганого (на відміну від, наприклад, Itanium, де регістри можуть бути позначені як недійсні , так що читання поширює помилки розповсюдження, як NaN).

Однак існують дві основні проблеми:

  1. Це не буде особливо випадково. У цьому випадку ви читаєте зі стека, тож отримаєте все, що там було раніше. Який може бути фактично випадковий, повністю структурований, пароль, який ви ввели десять хвилин тому, або рецепт печива вашої бабусі.

  2. Погано (велика літера "В") вводити такі речі, як цей, повзає у свій код. Технічно компілятор міг вставляти reformat_hdd();кожен раз, коли ви читаєте невизначену змінну. Це не стане , але все одно не варто робити цього. Не робіть небезпечних речей. Чим менше винятків ви робите, тим безпечніше ви від випадкових помилок все час.

Більш нагальною проблемою для UB є те, що це робить поведінку всієї вашої програми невизначеною. Сучасні компілятори можуть використовувати це, щоб уникнути величезних фрагментів вашого коду або навіть повернутися назад у часі . Грати з UB - це як вікторіанський інженер, що демонтує живий ядерний реактор. Порушується мільйон речей, і ви, мабуть, не знаєте половини основних принципів чи впровадженої технології. Це може бути нормально, але ти все одно не повинен дозволяти цього. Подивіться на інші приємні відповіді для деталей.

Також я б тебе звільнив.


39
@Potatoswatter: Ітанієві регістри можуть містити NaT (не річ), що фактично є "неініціалізованим реєстром". На Itanium, читання з реєстру, коли ви не написали до нього, може перервати вашу програму (докладнішу інформацію про неї можна прочитати тут: blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx ). Тож є вагома причина, чому читання неініціалізованих значень - це невизначена поведінка. Це, мабуть, одна з причин, чому Itanium не дуже популярний :)
tbleher

58
Я дійсно заперечую проти поняття "це працює". Навіть якби це було правдою сьогодні, а це не так, це може змінитися в будь-який час через більш агресивних компіляторів. Компілятор може замінити будь-яке прочитане unreachable()і видалити половину вашої програми. Це трапляється і на практиці. Я вважаю, що така поведінка повністю нейтралізувала RNG у деяких дистрибутивах Linux; Більшість відповідей на це питання, здається, припускають, що неініціалізована цінність поводиться як цінність взагалі. Це помилково.
usr

25
Крім того, я б звільнив вас, здається, досить нерозумно сказати, якщо припустити, що передовий досвід цей процес повинен бути зафіксований при перегляді коду, обговоренні і більше ніколи не повториться. Це, безумовно, має бути спійманим, оскільки ми використовуємо правильні попереджувальні прапори, правда?
Шафік Ягмур

17
@Michael Власне, так і є. Якщо програма має невизначене поведінку в будь-який момент, компілятор може оптимізувати вашу програму таким чином, що впливає на код, що передує виклику невизначеної поведінки. Існують різні статті та демонстрації того, як це може призвести до глузливих помилок. Ось непоганий варіант: blogs.msdn.com/b/oldnewthing/archive/2014/06/27/10537746.aspx (який включає біт у стандарт, що говорить всі ставки вимкнено, якщо будь-який шлях у вашій програмі посилається на UB)
Том Таннер,

19
Ця відповідь звучить так, ніби «посилатися на невизначене поведінку погано в теорії, але це насправді не зашкодить тобі на практиці» . Це неправильно. Збір ентропії з виразу, який спричинив би UB, може (і, ймовірно, призведе ) до втрати всі раніше зібраної ентропії . Це серйозна небезпека.
Теодорос Чатзіґянакікіс

213

Дозвольте сказати це чітко: ми не посилаємось на невизначену поведінку в наших програмах . Це ніколи не є хорошою ідеєю, періодом. Є рідкісні винятки з цього правила; наприклад, якщо ви реалізатор бібліотеки, який реалізує офсетоф . Якщо ваша справа підпадає під такий виняток, ви, ймовірно, це вже знаєте. У цьому випадку ми знаємо, що використання неініціалізованих автоматичних змінних є невизначеною поведінкою .

Компілятори стали дуже агресивними з оптимізаціями навколо невизначеної поведінки, і ми можемо знайти багато випадків, коли невизначена поведінка призвела до вад безпеки. Найвідоміший випадок - це, мабуть, видалення нульового вказівника ядра Linux, яке я згадую у своїй відповіді на помилку компіляції C ++? де оптимізація компілятора навколо невизначеної поведінки перетворила кінцевий цикл у нескінченний.

Ми можемо прочитати небезпечну оптимізацію та втрату причинності CERT ( відео ), в якому, серед іншого, сказано:

Все частіше автори-компілятори використовують переваги невизначеної поведінки на мовах програмування C та C ++ для покращення оптимізації.

Найчастіше ці оптимізації заважають можливості розробників виконувати аналіз причинно-наслідкового аналізу над своїм вихідним кодом, тобто аналізуючи залежність подальших результатів від попередніх результатів.

Отже, ці оптимізації усувають причинність у програмному забезпеченні та збільшують ймовірність помилок, дефектів та вразливих програм.

Зокрема, щодо невизначених значень стандартний звіт про дефекти С 451: Нестабільність неініціалізованих автоматичних змінних викликає цікаве прочитання. Це ще не було вирішено, але вводиться поняття коливальних значень, що означає, що невизначеність значення може поширюватися через програму і може мати різні невизначені значення в різних точках програми.

Я не знаю жодного прикладу, коли це відбувається, але на даний момент ми не можемо цього виключити.

Реальні приклади, а не результат, який ви очікуєте

Ви навряд чи отримаєте випадкові значення. Компілятор може повністю оптимізувати віддалений цикл. Наприклад, у цьому спрощеному випадку:

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

кланг оптимізує його подалі ( дивіться його наживо ):

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

або можливо отримати всі нулі, як у цьому модифікованому випадку:

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

побачити його наживо :

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

Обидва ці випадки є цілком прийнятними формами невизначеної поведінки.

Зауважте, якби ми знаходимось на Itanium, ми могли б отримати значення пастки :

[...] якщо в реєстрі відбувається спеціальне несуттєве значення, читаючи пастки регістра, за винятком кількох інструкцій [...]

Інші важливі зауваження

Цікаво відзначити різницю між gcc та clang, відзначеною в проекті UB Canaries, щодо того, наскільки вони готові скористатися невизначеною поведінкою щодо неініціалізованої пам'яті. Стаття зазначає ( моє наголос ):

Звичайно, ми повинні бути повністю зрозуміли, що будь-яке таке очікування не має нічого спільного з мовним стандартом, і все, що стосується того, що відбувається з певним компілятором, або тому, що постачальники цього компілятора не бажають використовувати цей UB або просто тому що вони ще не взялися до експлуатації . Коли жодної реальної гарантії від постачальника компіляторів не існує, ми хочемо сказати, що поки що невикористані UB - це бомби часу : вони чекають виходу з наступного місяця чи наступного року, коли компілятор стане трохи більш агресивним.

Як зазначає Матьє М., що кожен програміст C повинен знати про не визначену поведінку №2 / 3 , також має відношення до цього питання. У ньому сказано серед іншого ( наголос мій ):

Важливо і страшно зрозуміти, що практично будь-яка оптимізація, заснована на не визначеній поведінці, може почати спрацьовувати на баггі-код у будь-який час у майбутньому . Розгортання циклу, розгортання циклу, розширення пам’яті та інші оптимізації продовжуватимуть покращуватися, і значна частина їхніх причин існує в тому, щоб відкрити вторинні оптимізації, як описано вище.

Для мене це глибоко невдоволено, частково через те, що компілятор неминуче закінчується звинуваченням, але також тому, що це означає, що величезні тіла коду С - це наземні міни, які лише чекають вибуху.

Для повноти я, мабуть, повинен зазначити, що реалізація може вибрати чітко визначену поведінку, наприклад, gcc дозволяє набирати покарання через союзи, тоді як у C ++ це здається невизначеною поведінкою . У такому випадку реалізація повинна документувати це, і зазвичай це не буде портативним.


1
+ (int) (PI / 3) для прикладів виводу компілятора; в реальному житті приклад, UB, ну, UB .

2
Використання UB ефективно використовувалося як торгова марка відмінного хакера. Ця традиція триває вже, мабуть, 50 і більше років. На жаль, від комп’ютерів тепер потрібно мінімізувати наслідки UB через поганих людей. Мені дуже подобалося розбиратися, як робити цікаві речі з машинним кодом UB або читанням / записом портів і т. Д. Я 90-х, коли ОС не була здатна захистити користувача від себе.
sfdcfox

1
@sfdcfox, якщо ви робили це в машинному коді / асемблері, це була не визначена поведінка (це може бути нетрадиційна поведінка).
Калет

2
Якщо ви маєте на увазі певну збірку, тоді використовуйте це і не пишіть невідповідний C. Тоді всі будуть знати, що ви використовуєте певний трюк. І це не погані люди, які означають, що ви не можете використовувати UB, адже Intel і т.д. роблять свої трюки на чіпі.
Калет

2
@ 500-InternalServerError, оскільки вони можуть бути неважко виявити або взагалі не виявлятись у загальному випадку, і тому не було б можливості їх заборонити. Що відрізняється від порушень граматики, які можна виявити. У нас також є неправильно сформована і неправильно сформована діагностика, яка, як правило, відокремлює погано сформовані програми, які можна було б виявити в теорії від тих, які теоретично не могли бути надійно виявлені.
Шафік Ягмур

164

Ні, це жахливо.

Поведінка використання неініціалізованої змінної не визначена як у C, так і в C ++, і дуже малоймовірно, що така схема матиме бажані статистичні властивості.

Якщо ви хочете "швидкий і брудний" генератор випадкових чисел, то rand()це ваша найкраща ставка. У його реалізації все, що вона робить, - це множення, додавання та модуль.

Найшвидший генератор, про який я знаю, вимагає, щоб ви використовували uint32_tяк тип псевдовипадкової змінної Iта використовували

I = 1664525 * I + 1013904223

генерувати послідовні значення. Ви можете вибрати будь-яке початкове значення I(називається насінням ), яке захоплює вас. Очевидно, ви можете кодувати цей рядок. Модуль виступає в якості гарантованого стандартом обгортання неподписаного типу. (Числові константи підбирає вручну той чудовий науковий програміст Дональд Кнут.)


9
"Лінійний конгруентний" генератор, який ви представляєте, хороший для простих програм, але лише для некриптографічних додатків. Можна передбачити її поведінку. Дивіться, наприклад, " Розшифровка лінійного конгруентного шифрування " самого Дон Кнута (транзакції IEEE з інформаційної теорії, том 31)
Jay

24
@Jay порівняно з неіціалізованою змінною для швидкої та брудної? Це набагато краще рішення.
Майк Макмахон

2
rand()не підходить за призначенням і, на мій погляд, має бути повністю застарілим. У наші дні ви можете завантажувати вільно ліцензовані та надзвичайно чудові генератори випадкових чисел (наприклад, Mersenne Twister), які дуже швидко настільки ж швидко і з найбільшою легкістю, що дійсно не потрібно продовжувати користуватися сильно несправнимиrand()
Джек Едлі

1
rand () має ще одну жахливу проблему: він використовує своєрідний замок, який називається всередині потоків, він різко уповільнює ваш код. Принаймні, є рецензована версія. І якщо ви використовуєте C ++ 11, випадковий API надає все необхідне.
Marwan Burelle

4
Якщо чесно, він не запитав, чи це хороший генератор випадкових чисел. Він запитав, чи швидко це. Ну так, це, мабуть, голодно. Але результати будуть зовсім не випадковими.
jcoder

42

Гарне питання!

Невизначений не означає, що він випадковий. Подумайте про це, значення, які ви отримаєте у глобальних неініціалізованих змінних, залишила там система або ваші / інші програми. Залежно від того, що робить ваша система з уже не використовуваною пам'яттю та / або які значення генерують система та додатки, ви можете отримати:

  1. Завжди те саме.
  2. Будьте одним із невеликого набору значень.
  3. Отримуйте значення в одному або декількох невеликих діапазонах.
  4. Дивіться багато значень, які можна розділити на 2/4/8 від покажчиків на 16/32/64-бітній системі
  5. ...

Значення, які ви отримаєте, повністю залежать від того, які невипадкові значення залишені системою та / або програмами. Так, дійсно буде чути шум (якщо ваша система не стирає вже не використовувану пам'ять), але пул значень, з якого ви будете черпати, аж ніяк не буде випадковим.

Дело стає набагато гіршим для локальних змінних, оскільки вони надходять безпосередньо із стека вашої власної програми. Є дуже хороший шанс, що ваша програма насправді запише ці місця стеків під час виконання іншого коду. Я вважаю, що шанси на удачу в цій ситуації дуже низькі, і ви намагаєтесь "випадково" змінити код.

Прочитайте про випадковість . Як ви побачите, випадковість є дуже специфічною і важко отримати властивість. Поширеною помилкою є думка, що якщо ви просто візьмете щось, що важко відстежити (наприклад, ваша пропозиція), ви отримаєте випадкове значення.


7
... і це виключає всі оптимізатори компілятора, які б цілком кинули цей код.
Дедуплікатор

6 ... Ви отримаєте різні "випадковість" у "Налагодження" та "Випуск". Невизначений означає, що ви робите це неправильно.
Sql Surfer

Правильно. Я скорочу або підсумую "undefined"! = "Довільний"! = "Випадковий". Всі ці види «невідомості» мають різні властивості.
fche

Гарантовано, що глобальні змінні матимуть визначене значення, явно ініціалізовані чи ні. Це, безумовно , вірно в C ++ і в C , а також .
Брайан Ванденберг

32

Багато хороших відповідей, але дозвольте додати ще одну і наголошу на тому, що в детермінованому комп’ютері нічого не є випадковим. Це справедливо як для чисел, що створюються псевдо-RNG, так і, здавалося б, "випадковими" числами, знайденими в областях пам'яті, відведених для локальних змінних C / C ++ на стеку.

АЛЕ ... є вирішальна різниця.

Числа, що створюються хорошим псевдовипадковим генератором, мають властивості, які роблять їх статистично подібними до справді випадкових малюнків. Наприклад, розподіл є рівномірним. Тривалість циклу довга: ви можете отримати мільйони випадкових чисел до того, як цикл повториться. Послідовність не є автокорельованою: наприклад, ви не почнете бачити дивні візерунки, якщо ви візьмете кожне 2-е, 3-е чи 27-е число або якщо будете дивитися на певні цифри в згенерованих числах.

На відміну від цього, "випадкові" числа, залишені на стеку, не мають жодної з цих властивостей. Їх значення та їх очевидна випадковість повністю залежать від того, як побудована програма, як вона складена та як її оптимізує компілятор. В якості прикладу, ось варіант вашої ідеї як самодостатньої програми:

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

Коли я компілюю цей код з GCC на машині Linux і запускаю його, він виявляється досить неприємно детермінованим:

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

Якщо ви подивилися складений код за допомогою розбиральника, ви могли реконструювати, що відбувається, докладно. Перший виклик до notrandom () використовував область стека, яка раніше не використовувалася цією програмою; хто знає, що там було. Але після цього виклику до notrandom (), відбувається виклик до printf () (який компілятор GCC насправді оптимізує до виклику putchar (), але нічого не важливо) і це перезаписує стек. Отже, наступний і наступний час, коли викликається notrandom (), стек буде містити застарілі дані від виконання putchar (), а оскільки putchar () завжди викликається з однаковими аргументами, ці застарілі дані завжди будуть однаковими, теж.

Тож немає абсолютно нічого випадкового щодо такої поведінки, а також числа, отримані таким чином, не мають жодних бажаних властивостей добре написаного генератора псевдовипадкових чисел. Насправді, у більшості реальних сценаріїв їх значення будуть повторюватися та сильно співвідноситись.

Дійсно, як і інші, я також серйозно розглядав би можливість звільнення того, хто намагався передати цю ідею як "високоефективний РНГ".


1
"У детермінованому комп'ютері нічого не є випадковим" - Це насправді не так. Сучасні комп'ютери містять всі види датчиків, які дозволяють виробляти справжню , непередбачувану випадковість без окремих апаратних генераторів. У сучасній архітектурі величини /dev/randomчасто походять з таких апаратних джерел і насправді є "квантовим шумом", тобто справді непередбачуваним у кращому фізичному сенсі цього слова.
Конрад Рудольф

2
Але тоді, це не детермінований комп'ютер, чи не так? Тепер ви покладаєтесь на внесок у навколишнє середовище. У будь-якому випадку це виводить нас поза рамки обговорення звичайного псевдо-RNG проти "випадкових" бітів у неініціалізованій пам'яті. Також ... погляньте на опис / dev / random, щоб оцінити, наскільки далеко вони зі свого шляху пішли на реалізацію, щоб переконатися, що випадкові числа криптографічно захищені ... саме тому, що джерела введення не є чистими, некорельованим квантовим шумом, але швидше, потенційно сильно корельовані показання сенсорів лише з невеликим ступенем випадковості. Це теж повільно.
Віктор Тот

29

Невизначена поведінка означає, що автори компіляторів можуть ігнорувати проблему, оскільки програмісти ніколи не матимуть права скаржитися на те, що трапиться.

Хоча теоретично при в'їзді на землю UB може статися все, що завгодно (включаючи демона, який летить з вашого носа ), що зазвичай означає, що авторам-компіляторам просто не буде байдуже, а для локальних змінних значенням буде те, що є в пам'яті стека на той момент. .

Це також означає, що часто вміст буде "дивним", але фіксованим або дещо випадковим або змінним, але з чіткою очевидною схемою (наприклад, збільшення значень при кожній ітерації).

Напевно, ви не можете очікувати, що він буде гідним випадковим генератором.


28

Невизначена поведінка не визначена. Це не означає, що ви отримуєте невизначене значення, це означає, що програма може робити все, що завгодно, і все ще відповідає мовній специфікації.

Хороший оптимізаційний компілятор повинен взяти

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

і компілювати його в noop. Це, звичайно, швидше, ніж будь-яка альтернатива. Мінус у тому, що він нічого не зробить, але такий недолік невизначеної поведінки.


3
Багато залежить від того, чи є мета компілятора допомогти програмістам у створенні виконуваних файлів, що відповідають доменним вимогам, чи ж метою є створення найефективнішого виконуваного файлу, поведінка якого буде відповідати мінімальним вимогам стандарту С, без врахуйте, чи буде така поведінка корисною метою. Що стосується колишньої мети, використання коду використовувати деякі довільні початкові значення для r, g, b або запускати пастку налагодження, якщо це практично, було б кориснішим, ніж перетворення коду в nop. Що стосується останньої мети ...
supercat

2
... оптимальний компілятор повинен визначити, які входи спричинить виконання вищевказаного методу, і усунути будь-який код, який був би актуальним лише при отриманні таких входів.
supercat

1
@supercat Або його метою може бути C. створення ефективних виконуваних файлів відповідно до Стандарту, допомагаючи програмісту знайти місця, де відповідність може бути не корисною. Компілятори можуть досягти цієї компромісної мети, провівши більше діагностики, ніж вимагає Стандарт, наприклад, ГКЗ -Wall -Wextra.
Damian Yerrick

1
Те, що значення не визначені, не означає, що поведінка оточуючого коду не визначена. Жоден компілятор не повинен виконувати цю функцію. Дві функції дзвінка, незалежно від того, які входи вони надані, ОБОВ'ЯЗКОВО ОБОВ'ЯЗКОВИ; перший ОБОВ'ЯЗКОВО викликати з трьома числами між 0 і 255, а другий ОБОВ'ЯЗКОВО називатися з істинним або хибним значенням. "Хороший оптимізуючий компілятор" міг оптимізувати параметри функції до довільних статичних значень, позбувшись змінних повністю, але це настільки, наскільки це могло б піти (ну хіба що, якщо самі функції не могли бути зведені до шумів на певних входах).
Деві Морган

@DewiMorgan - оскільки виклики функцій типу "встановити цей параметр", вони майже напевно зменшують до шумів, коли вхідне значення відповідає поточному значенню параметра, яке компілятор вільний вважати.
Жуль

18

Ще не згадано, але шляхи коду, які викликають невизначене поведінку, дозволяють робити все, що компілятор хоче, наприклад

void updateEffect(){}

Що, безумовно, швидше, ніж ваш правильний цикл, і через UB, цілком відповідає.


18

З міркувань безпеки нову пам'ять, призначену програмі, потрібно очистити, інакше інформація може використовуватися, а паролі можуть просочуватися з однієї програми в іншу. Тільки при повторному використанні пам’яті ви отримуєте різні значення, ніж 0. І дуже ймовірно, що на стеці попереднє значення просто фіксується, оскільки попереднє використання цієї пам’яті фіксовано.


13

Ваш конкретний приклад коду, ймовірно, не буде робити те, що ви очікуєте. Хоча технічно кожна ітерація циклу відновлює локальні змінні для значень r, g та b, на практиці це точно такий же простір пам’яті на стеку. Таким чином, вона не буде повторно рандомізована з кожною ітерацією, і ви в кінцевому підсумку призначите однакові 3 значення для кожного з 1000 кольорів, незалежно від того, наскільки випадкові r, g і b індивідуально і спочатку.

Дійсно, якби це спрацювало, мені було б дуже цікаво, що це повторно рандомізує. Єдине, про що я можу придумати, - це переплетене переривання, яке поросяти на вершині цього стека, дуже малоймовірно. Можливо, внутрішня оптимізація, яка зберігала б їх як змінні реєстру, а не як справжні локації пам'яті, де регістри повторно використовуються далі в циклі, теж зробила б свою справу, особливо якщо встановлена ​​функція видимості особливо є голодною до реєстру. Все-таки далеко не випадково.


12

Оскільки більшість людей тут згадували невизначену поведінку. Невизначений також означає, що ви можете отримати деяке дійсне ціле значення (на щастя), і в цьому випадку це буде швидше (оскільки виклик функції rand не робиться). Але практично не використовуйте. Я впевнений, що це матиме жахливі результати, оскільки удача не з вами весь час.


1
Дуже гарна точка! Це може бути прагматичний трюк, але дійсно той, який вимагає удачі.
сенс має значення

1
Немає жодної удачі. Якщо компілятор не оптимізує невизначене поведінку вдалині, отримані вами значення будуть ідеально детермінованими (= повністю залежать від вашої програми, її входів, її компілятора, бібліотек, які вона використовує, часу його потоків, якщо у них є потоки). Проблема полягає в тому, що ви не можете міркувати про ці значення, оскільки вони залежать від деталей реалізації.
cmaster - відновити моніку

За відсутності операційної системи зі стеком обробки перебоїв, окремим від стека додатків, удача може бути задіяна, оскільки переривання часто порушують вміст пам'яті трохи вище поточного вмісту стека.
supercat

12

Справді погано! Погана звичка, поганий результат. Поміркуйте:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

Якщо функція A_Function_that_use_a_lot_the_Stack()робить завжди однакову ініціалізацію, вона залишає стек з однаковими даними про неї. Ці дані ми називаємо updateEffect(): завжди однакове значення! .


11

Я провів дуже простий тест, і він зовсім не був випадковим.

#include <stdio.h>

int main() {

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

Щоразу, коли я запускав програму, вона друкувала однакову кількість ( 32767у моєму випадку) - ви не можете отримати набагато менше випадкових випадків. Це, мабуть, незалежно від стартового коду в бібліотеці виконання, який залишився у стеці. Оскільки він використовує один і той же стартовий код кожного разу, коли програма запускається, і більше нічого не змінюється в програмі між запусками, результати цілком узгоджуються.


Гарна думка. Результат сильно залежить від того, де в коді викликається цей "випадковий" генератор чисел. Це скоріше непередбачувано, ніж випадково.
NO_NAME

10

Потрібно визначити, що ви маєте на увазі під "випадковим". Розумне визначення передбачає, що отримані значення повинні мати невелике співвідношення. Це те, що можна виміряти. Це також не банально досягти контрольованим, відтворюваним чином. Тож невизначена поведінка - це точно не те, що ви шукаєте.


7

Існують певні ситуації, коли неініціалізовану пам'ять можна безпечно читати, використовуючи тип "неподписаний знак *" [наприклад, буфер, повернутий з malloc]. Код може читати таку пам'ять, не турбуючись про те, що компілятор викидає причинність у вікно, і бувають випадки, коли може бути більш ефективним підготувати код до всього, що може містити пам'ять, ніж гарантувати, що неініціалізовані дані не будуть прочитані ( звичайним прикладом цього є використання memcpyна частково ініціалізованому буфері, а не дискретно копіювання всіх елементів, що містять змістовні дані).

Навіть у таких випадках, однак, завжди слід вважати, що якщо будь-яка комбінація байтів буде особливо вразливою, її читання завжди дасть таку схему байтів (і якщо певна закономірність буде неприємною у виробництві, але не в розвитку, такий шаблон не з’явиться, поки код не з’явиться у виробництві).

Читання неініціалізованої пам’яті може бути корисним як частина стратегії випадкового покоління у вбудованій системі, де можна бути впевненим, що пам’ять ніколи не була записана з по суті не випадковим вмістом з моменту останнього ввімкнення системи, і якщо виробництво Процес, що використовується для пам'яті, призводить до того, що стан його включення змінюється напіввипадковим чином. Код повинен працювати, навіть якщо всі пристрої завжди отримують однакові дані, але у випадках, коли, наприклад, група вузлів потребує вибору довільних унікальних ідентифікаторів якомога швидше, маючи генератор "не дуже випадкових", який дає половині вузлів однакові початкові Ідентифікатор може бути кращим, ніж взагалі не мати початкового джерела випадковості.


2
"якщо будь-яка комбінація байтів буде особливо неприємною, її читання завжди призведе до цього шаблону байтів", - поки ви не введете код, щоб впоратися з цим шаблоном, і тоді він більше не буде неспокійним, і в майбутньому буде прочитаний інший шаблон.
Стів Джессоп

@SteveJessop: Саме так. Мій рядок про розвиток проти виробництва мав на меті передати подібне поняття. Код не повинен перейматися тим, що знаходиться в неініціалізованій пам'яті за межами розпливчастого поняття "якась випадковість може бути приємною". Якщо на поведінку програми впливає вміст одного фрагмента неініціалізованої пам’яті, на це може вплинути вміст фрагментів, які придбаються в майбутньому.
supercat

5

Як говорили інші, це буде швидко, але не випадково.

Що більшість компіляторів зробить для локальних змінних - це захопити деякий простір для них на стеці, але не заважати встановлювати його ні на що (стандарт стверджує, що цього не потрібно, тож чому сповільнювати код, який ви генеруєте?).

У цьому випадку значення, яке ви отримаєте, буде залежати від того, що було раніше в стеці - якщо ви викличете функцію перед цією, яка має сто локальних змінних знаків, всі встановлені на "Q", а потім зателефонуєте, що ви функціонуєте після що повертається, то, ймовірно, ви виявите, що ваші "випадкові" значення поводяться так, ніби ви memset()їх усі до "Q".

Важливо, що для вашої прикладної функції, яка намагається використовувати це, ці значення не змінюватимуться кожного разу, коли ви їх читаєте, вони будуть однакові щоразу. Таким чином, ви отримаєте 100 зірок, всі налаштовані на один і той же колір і видимість.

Крім того, нічого не говорить про те, що компілятор не повинен ініціалізувати ці значення - тому майбутній компілятор може це зробити.

Взагалі: погана ідея, не робіть цього. (як і справді багато "розумних" оптимізацій рівня коду ...)


2
Ви робите сильні прогнози щодо того , що станеться, хоча нічого з цього не гарантується завдяки UB. Це також не відповідає дійсності.
usr

3

Як уже згадували інші, це невизначена поведінка ( UB ), але це може "спрацювати".

За винятком проблем, про які вже згадували інші, я бачу ще одну проблему (недолік) - вона не працюватиме будь-якою мовою, окрім C та C ++. Я знаю, що це питання стосується C ++, але якщо ви можете написати код, який буде хорошим C ++ та Java кодом, і це не проблема, то чому б ні? Можливо, колись комусь доведеться перенести його на іншу мову і пошук помилок, викликаних "магічними трюками" UB, подібний, безумовно, буде кошмаром (особливо для недосвідченого розробника C / C ++).

Тут виникає питання про ще один подібний UB. Уявіть собі, що намагаєтеся знайти подібну помилку, не знаючи про це UB. Якщо ви хочете почитати більше про такі дивні речі на C / C ++, прочитайте відповіді на запитання за посиланням і подивіться це ВЕЛИКЕ слайд-шоу. Це допоможе вам зрозуміти, що знаходиться під кришкою і як це працює; це не просто чергове слайд-шоу, сповнене "магії". Я впевнений, що навіть більшість досвідчених програмістів на C / c ++ можуть багато чому навчитися.


3

Не дуже гарна ідея покладатися на будь-яку логіку на мовну невизначеність поведінки. На додаток до всього, що згадувалося / обговорювалося в цій публікації, я хотів би зазначити, що за сучасного C ++ підходу / стилю така програма може бути не складена.

Про це було сказано в моєму попередньому дописі, який містить перевагу автоматичної функції та корисне посилання на те саме.

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

Отже, якщо ми змінимо вищевказаний код і замінимо фактичні типи на auto , програма навіть не буде компілювати.

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

Мені подобається твій спосіб мислення. Дійсно поза коробкою. Однак компроміс справді не вартий цього. Компроміс у режимі пам'яті - це річ, у тому числі не визначена поведінка під час виконання не є .

Це повинно дати вам дуже неспокійне відчуття, що ви використовуєте такий "випадковий", як ваша логіка бізнесу. Я б не робив цього.


3

Використовуйте 7757кожне місце, де вам спокуса використовувати неініціалізовані змінні. Я вибрав його випадковим чином із списку простих чисел:

  1. це визначена поведінка

  2. гарантовано не завжди буде 0

  3. це прем'єр

  4. це, ймовірно, буде настільки ж статистично випадковим, як неінтуіалізовані змінні

  5. це може бути швидше, ніж неініціалізовані змінні, оскільки його значення відоме під час компіляції


Для порівняння дивіться результати у цій відповіді: stackoverflow.com/a/31836461/2963099
Glenn Teitelbaum

1

Є ще одна можливість розглянути.

Сучасні компілятори (ах г ++) настільки розумні, що вони проходять ваш код, щоб побачити, які вказівки впливають на стан, а що ні, і якщо гарантія, що НЕ вплине на стан, g ++ просто видалить цю інструкцію.

Тож ось що буде. g ++ обов'язково побачить, що ви читаєте, виконуючи арифметику, економлячи, що по суті є значенням сміття, яке створює більше сміття. Оскільки немає гарантії, що нове сміття корисніше, ніж старе, воно просто усуне вашу петлю. BLOOP!

Цей метод корисний, але ось що я б робив. Поєднайте UB (не визначена поведінка) зі швидкістю rand ().

Звичайно, зменшіть rand()виконувані s, але змішайте їх, щоб компілятор не робив нічого, чого не хочете.

І я не буду звільняти тебе.


Мені дуже важко повірити, що компілятор може вирішити, що ваш код робить щось нерозумно, і видалити його. Я б очікував, що це лише оптимізувати невикористаний код , а не недоцільний код. Чи є у вас відтворюваний тестовий випадок? Так чи інакше, рекомендація UB небезпечна. Крім того, GCC не є єдиним компетентним компілятором, тому несправедливо називати його "сучасним".
підкреслити_10

-1

Використання неініціалізованих даних для випадкових випадків - це не обов'язково, якщо це зроблено належним чином. Насправді, OpenSSL робить саме це, щоб створити свій PRNG.

Мабуть, це використання було недостатньо задокументоване, тому що хтось помітив, що Вальгрінд скаржився на використання неініціалізованих даних та "виправляв" їх, викликаючи помилку в PRNG .

Тож ви можете це зробити, але вам потрібно знати, що ви робите, і переконатися, що хтось, хто читає ваш код, це розуміє.


1
Це буде залежати від вашого компілятора, який очікується з невизначеною поведінкою, як ми бачимо, з моєї відповіді кланг сьогодні не буде робити те, що вони хочуть.
Шафік Ягмур

6
Те, що OpenSSL використовував цей метод як введення ентропії, не говорить про те, що це було корисно. Зрештою, єдиним іншим джерелом ентропії, яким вони користувалися, був PID . Не зовсім гарне випадкове значення. Від того, хто покладається на таке погане джерело ентропії, я не очікую хорошого судження щодо їх іншого джерела ентропії. Я просто сподіваюся, що люди, які зараз підтримують OpenSSL, яскравіші.
cmaster - відновити моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.