Неинициализированная локальная переменная - самый быстрый генератор случайных чисел?
Я знаю, что неинициализированная локальная переменная - это поведение undefined (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);
}
}
а также быстрее, чем другой генератор случайных чисел?
Ответы
Ответ 1
Как отмечали другие, это Undefined Поведение (UB).
На практике это (возможно) фактически (kindof) работает. Чтение из неинициализированного регистра на архитектуре x86 [-64] действительно приведет к результатам мусора и, вероятно, не сделает ничего плохого (в отличие от, например, Itanium, где регистры могут быть помечены как недопустимые, так что чтение распространяется на ошибки, такие как NaN).
Есть две основные проблемы:
-
Это не будет особенно случайным. В этом случае вы читаете из стека, так что вы получите все, что было ранее. Который может быть эффективно случайным, полностью структурированным, пароль, который вы ввели десять минут назад, или рецепт вашей бабушки cookie.
-
Это плохо (капитал "B" ), чтобы позволить подобным образом проникнуть в ваш код. Технически компилятор может вставлять reformat_hdd();
каждый раз, когда вы читаете переменную Undefined. Это не так, но вы все равно не должны этого делать. Не делайте небезопасные вещи. Чем меньше исключений вы делаете, тем более безопасны вы от случайных ошибок все время.
Более насущная проблема с UB заключается в том, что она полностью управляет вашей программой undefined. Современные компиляторы могут использовать это, чтобы преодолеть огромные полосы вашего кода или даже вернуться во времени. Игра с UB похожа на викторианского инженера, который демонтирует живой ядерный реактор. Там что-то не так, и вы, вероятно, не будете знать половину основополагающих принципов или внедренных технологий. Это может быть хорошо, но вы все равно не должны позволять этому случиться. Посмотрите на другие приятные ответы для деталей.
Кроме того, я бы уволил вас.
Ответ 2
Позвольте мне сказать это четко: мы не вызываем поведение undefined в наших программах. Это никогда не бывает хорошей идеей, периодом. Редкие исключения из этого правила; например, если вы являетесь разработчиком библиотеки, реализующим offsetof. Если ваше дело попадает под такое исключение, вы, вероятно, знаете это уже. В этом случае мы знаем, что использование неинициализированных автоматических переменных - это поведение undefined.
Компиляторы стали очень агрессивными с оптимизацией вокруг поведения undefined, и мы можем найти множество случаев, когда поведение undefined привело к нарушениям безопасности. Самый печально известный случай - это, вероятно, удаление нулевого указателя ядра ядра Linux, о котором я упоминаю в мой ответ на С++ ошибка компиляции?, где оптимизация компилятора вокруг поведения undefined превратила конечный цикл в бесконечный.
Мы можем читать CERT Опасные оптимизации и потеря причинности, в которых, среди прочего, говорится:
Все чаще авторы сценариев используют undefinedповедения на языках программирования C и С++ для улучшения оптимизаций.
Часто эти оптимизации мешают способность разработчиков выполнять причинно-следственный анализ на их исходный код, то есть анализ зависимости результатов ниже по течению по предыдущим результатам.
Следовательно, эти оптимизации устраняются причинности в программном обеспечении и повышают вероятность программного обеспечения ошибок, дефектов и уязвимостей.
В частности, в отношении неопределенных значений, отчет о дефекте видеть его в прямом эфире):
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
Оба этих случая являются вполне приемлемыми формами поведения undefined.
Обратите внимание, что если мы находимся на Itanium, мы могли бы получить значение trap:
[...], если регистр имеет специальное значение не-вещь, чтение ловушек регистра, за исключением нескольких инструкций [...]
Другие важные примечания
Интересно отметить дисперсию между gcc и clang, отмеченную в проекте UB Canaries, о том, как они хотят использовать undefined по отношению к неинициализированной памяти. В статье отмечается (внимание мое):
Конечно, мы должны полностью понять, что любое такое ожидание не имеет ничего общего с языковым стандартом и все, что связано с тем, что делает конкретный компилятор, либо потому, что поставщики этого компилятора не хотят использовать этот UB или просто потому, что они еще не использовали его. Если нет реальной гарантии от поставщика компилятора, , мы хотели бы сказать, что пока неиспользованные UB - это бомбы замедленного действия: theyre ждет, чтобы уйти в следующем месяце или в следующем году, когда компилятор становится немного более агрессивным.
Как указывает Matthieu M. Что должен знать каждый программист C undefined Поведение № 2/3, также относится к этому вопросу. В нем говорится, среди прочего (акцент мой):
Важная и страшная вещь, которую нужно осознать, состоит в том, что практически любой оптимизация на основе поведения undefined может запускаться buggy code в любое время в будущем. Встраивание, циклическая развертка, память продвижение и другие оптимизации будут улучшаться, и значительная часть их причины для существования заключается в том, чтобы оптимизации, такие как выше.
Для меня это глубоко неудовлетворительно, частично потому, что компилятор неизбежно оказывается обвиненным, но также потому, что это означает, что огромные тела C-кода - это наземные мины, ожидающие взорваться.
Для полноты я должен, вероятно, упомянуть, что реализации могут выбрать корректное поведение undefined, например gcc позволяет вводить пул через объединения а в С++ это похоже на поведение undefined. Если это так, реализация должна документировать его, и это обычно не будет переносимым.
Ответ 3
Нет, это ужасно.
Поведение использования неинициализированной переменной undefined как в C, так и в С++, и очень маловероятно, чтобы такая схема имела желательные статистические свойства.
Если вам нужен "быстрый и грязный" генератор случайных чисел, то rand()
- ваш лучший выбор. В его реализации все, что он делает, это умножение, добавление и модуль.
Самый быстрый генератор, о котором я знаю, требует использования uint32_t
в качестве типа псевдослучайной переменной I
и использования
I = 1664525 * I + 1013904223
для генерации последовательных значений. Вы можете выбрать любое начальное значение I
(называемое семенем), которое берет ваше воображение. Очевидно, вы можете закодировать этот встроенный. В качестве модуля действует стандартно гарантированное обертывание неподписанного типа. (Численные константы отбираются у этого замечательного научного программиста Дональда Кнута.)
Ответ 4
Хороший вопрос!
Undefined не означает, что он случайный. Подумайте об этом, значения, которые вы получили в глобальных неинициализированных переменных, оставались там системой или вашими/другими приложениями. В зависимости от того, что ваша система делает с более не используемой памятью и/или какие значения генерирует система и приложения, вы можете получить:
- Всегда то же самое.
- Будь одним из небольшого набора значений.
- Получить значения в одном или нескольких небольших диапазонах.
- См. множество значений, делящихся на 2/4/8 из указателей в 16/32/64-битной системе.
- ...
Значения, которые вы получите полностью, зависят от того, какие неслучайные значения оставлены системой и/или приложениями. Таким образом, действительно будет некоторый шум (если ваша система не уничтожит больше не используемую память), но пул значений, из которого вы рисуете, ни в коем случае не будет случайным.
Для локальных переменных ситуация становится намного хуже, потому что они поступают непосредственно из стека вашей собственной программы. Существует очень хороший шанс, что ваша программа будет фактически записывать эти позиции стека во время выполнения другого кода. Я оцениваю шансы на удачу в этой ситуации очень низко, и "случайное" изменение кода вы делаете попытку этой удачи.
Читайте о randomness. Как вы увидите, случайность - это очень специфическое и труднодоступное свойство. Это распространенная ошибка думать, что если вы просто возьмете то, что трудно отслеживать (например, ваше предложение), вы получите случайное значение.
Ответ 5
Много хороших ответов, но позвольте мне добавить еще один и подчеркнуть, что на детерминированном компьютере ничто не является случайным. Это справедливо как для чисел, создаваемых псевдо-RNG, так и для кажущихся "случайных" чисел, найденных в областях памяти, зарезервированных для локальных переменных 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() всегда вызывается с теми же аргументами, эти устаревшие данные всегда будут одинаковыми, тоже.
Таким образом, нет абсолютно ничего случайного в этом поведении, равно как и числа, полученные таким образом, не имеют каких-либо желательных свойств хорошо написанного генератора псевдослучайных чисел. Фактически, в большинстве реальных сценариев их значения будут повторяться и сильно коррелироваться.
Действительно, как и другие, я бы также серьезно подумал об увольнении кого-то, кто попытался передать эту идею как "высокопроизводительный RNG".
Ответ 6
Undefined поведение означает, что авторы компиляторов могут игнорировать проблему, потому что у программистов никогда не будет права жаловаться на происходящее.
Хотя в теории при входе в землю UB может случиться что-то (включая демон, летящий с вашего носа), что обычно означает, что авторам-компиляторам просто все равно, и для локальных переменных значение будет тем, что находится в памяти стека в этой точке.
Это также означает, что часто контент будет "странным", но фиксированным или слегка случайным или переменным, но с явным очевидным шаблоном (например, увеличение значений на каждой итерации).
Конечно, вы не можете ожидать, что это приличный случайный генератор.
Ответ 7
Undefined поведение undefined. Это не означает, что вы получаете значение undefined, это значит, что программа может что-то делать и все еще соответствует спецификации языка.
Хороший оптимизирующий компилятор должен принимать
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. Это, безусловно, быстрее, чем любая альтернатива. Недостатком является то, что он ничего не сделает, но это недостаток поведения undefined.
Ответ 8
Из-за соображений безопасности новая память, назначенная программе, должна быть очищена, в противном случае информация может быть использована, и пароли могут протекать из одного приложения в другое. Только при повторном использовании памяти вы получаете разные значения, чем 0. И очень вероятно, что в стеке предыдущее значение просто фиксировано, потому что предыдущее использование этой памяти исправлено.
Ответ 9
Не упоминается, но пути к коду, вызывающие поведение undefined, позволяют делать все, что хочет компилятор, например
void updateEffect(){}
Это, безусловно, быстрее, чем ваш правильный цикл, и из-за UB, идеально соответствует.
Ответ 10
Пример вашего конкретного кода, вероятно, не будет делать то, что вы ожидаете. Технически каждая итерация цикла повторно создает локальные переменные для значений r, g и b, на практике это то же самое пространство памяти в стеке. Следовательно, он не будет повторно рандомизирован с каждой итерацией, и вы в конечном итоге назначаете одинаковые 3 значения для каждого из 1000 цветов, независимо от того, насколько случайны r, g и b являются индивидуально и изначально.
В самом деле, если бы это сработало, мне было бы очень интересно узнать, что такое рандомизация. Единственное, о чем я могу думать, это чередование прерываний, которые могут быть поставлены на вершине этого стека, что маловероятно. Возможно, внутренняя оптимизация, которая сохранила их как регистровые переменные, а не как истинные ячейки памяти, где регистры снова используются в нижнем цикле, также будет хитростью, особенно если функция видимости в определенном режиме является голодной. Тем не менее, далеко не случайно.
Ответ 11
Как большинство людей упоминает поведение undefined. undefined также означает, что вы можете получить некоторое действительное целочисленное значение (к счастью), и в этом случае это будет быстрее (поскольку вызов функции rand не выполняется).
Но практически не используйте его. Я уверен, что это будет ужасным результатом, потому что удача не с вами все время.
Ответ 12
Действительно плохо! Плохая привычка, плохой результат.
Рассмотрим:
A_Function_that_use_a_lot_the_Stack();
updateEffect();
Если функция A_Function_that_use_a_lot_the_Stack()
делает всегда ту же инициализацию, она оставляет стек с теми же данными на нем. Эти данные являются тем, что мы вызываем updateEffect()
: всегда одинаковое значение!.
Ответ 13
Я выполнил очень простой тест, и он не был случайным вообще.
#include <stdio.h>
int main() {
int a;
printf("%d\n", a);
return 0;
}
Каждый раз, когда я запускал программу, она печатала то же число (32767
в моем случае) - вы не можете получить гораздо менее случайное, чем это. Это, по-видимому, независимо от кода запуска в библиотеке времени выполнения, оставшейся в стеке. Так как он использует один и тот же код запуска каждый раз, когда запускается программа, и ничто другое не меняется в программе между прогонами, результаты совершенно согласованы.
Ответ 14
Вам нужно определить, что вы подразумеваете под "случайным".
Разумное определение предполагает, что полученные вами значения должны иметь небольшую корреляцию. Это то, что вы можете измерить. Это также не является тривиальным для достижения в контролируемой, воспроизводимой манере. Таким образом, поведение undefined, конечно, не то, что вы ищете.
Ответ 15
Как говорили другие, он будет быстрым, но не случайным.
То, что большинство компиляторов сделает для локальных переменных, - это захватить некоторое пространство для них в стеке, но не утруждать себя настройкой на что-либо (стандарт говорит, что им это не нужно, поэтому зачем замедлять код, который вы генерируете?).
В этом случае значение, которое вы получите, будет зависеть от того, что было ранее в стеке - если вы вызываете функцию до этого, у которой есть сотня локальных переменных char, все установлены на "Q", а затем вызывают вы будете функционировать после того, как это вернется, тогда вы, вероятно, найдете, что ваши "случайные" значения ведут себя так, как если бы вы memset()
их всех в "Q".
Важно, что для вашего примера, использующего эту функцию, эти значения не меняются каждый раз, когда вы их читаете, они будут одинаковыми каждый раз. Таким образом, вы получите 100 звезд, все настроены на тот же цвет и видимость.
Кроме того, ничто не говорит о том, что компилятор не должен инициализировать это значение, поэтому будущий компилятор может это сделать.
В общем: плохая идея, не делай этого.
(как много "умных" оптимизаций уровня кода действительно...)
Ответ 16
Существуют определенные ситуации, в которых неинициализированная память может быть безопасно прочитана с использованием типа "unsigned char *" [например. буфер возвращается из malloc
]. Код может читать такую память, не беспокоясь о том, что компилятор бросает причинность из окна, и бывают случаи, когда может быть более эффективным создание кода для чего-либо, что может содержать память, чем для обеспечения того, чтобы неинициализированные данные не были прочитаны ( обычным примером этого будет использование memcpy
в частично инициализированном буфере, а не дискретное копирование всех элементов, содержащих содержательные данные).
Тем не менее, даже в таких случаях следует всегда предполагать, что если какая-либо комбинация байтов будет особенно досадной, чтение ее всегда будет приводить к тому, что паттерн байтов (и если какой-то образец будет досадным в производстве, но не в разработке, такой шаблон не будет отображаться до тех пор, пока код не будет создан).
Чтение неинициализированной памяти может быть полезно как часть стратегии случайного генерации во встроенной системе, где можно быть уверенным, что память никогда не была написана с существенно-неслучайным содержимым с момента последнего включения системы и если производственный процесс, используемый для памяти, приводит к тому, что состояние включения питания изменяется в полуслучайном порядке. Код должен работать, даже если все устройства всегда дают одни и те же данные, но в тех случаях, когда, например, группе узлов нужно как можно быстрее выбирать произвольные уникальные идентификаторы, имея "не очень случайный" генератор, который дает половину узлов один и тот же начальный идентификатор может быть лучше, чем не иметь никакого исходного источника случайности вообще.
Ответ 17
Как уже упоминалось, это поведение undefined (UB), но оно может "работать".
За исключением проблем, уже упомянутых другими, я вижу еще одну проблему (недостаток) - она не будет работать ни на одном другом языке, кроме C и С++. Я знаю, что этот вопрос касается С++, но если вы можете написать код, который будет хорошим С++ и Java-кодом, и это не проблема, то почему бы и нет? Возможно, когда-нибудь кому-то придется переносить его на другой язык и искать ошибки, вызванные "волшебными трюками" UB, как это определенно будет кошмаром (особенно для неопытного разработчика C/С++).
Здесь возникает вопрос о другом подобном UB. Представьте себе, что вы пытаетесь найти такую ошибку, не зная об этом UB. Если вы хотите больше узнать о таких странных вещах в C/С++, прочитайте ответы на вопрос из ссылки и посмотрите this GREAT слайд-шоу. Это поможет вам понять, что под капотом и как оно работает; это не просто еще одно слайд-шоу, полное "магии". Я вполне уверен, что даже большинство опытных программистов на C/С++ могут многое узнать из этого.
Ответ 18
Используйте 7757
каждое место, в которое вы соблазняетесь использовать неинициализированные переменные. Я выбрал его случайным образом из списка простых чисел:
-
определяется поведение
-
гарантировано не всегда 0
-
это просто
-
он, скорее всего, будет статистически случайным, как uninitualized
переменные
-
он, вероятно, будет быстрее, чем неинициализированные переменные, поскольку его
значение известно во время компиляции
Ответ 19
Не хорошая идея полагаться на нашу логику поведения языка undefined. В дополнение к тому, что упоминалось/обсуждалось в этом посте, я хотел бы упомянуть, что с современным подходом/стилем С++ такая программа не может компилироваться.
Это было упомянуто в моем предыдущем посте, в котором содержится преимущество функции Авто и полезной ссылки для нее.
fooobar.com/questions/20794/...
Итак, если мы изменим приведенный выше код и заменим фактические типы авто, программа даже не будет компилироваться.
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);
}
}
Ответ 20
Мне нравится ваш образ мыслей. Действительно вне коробки. Однако компромисс действительно не стоит этого. Компромисс между памятью и временем выполнения, в том числе поведение undefined для среды выполнения не.
Это должно дать вам очень тревожное ощущение, что вы используете такие "случайные", как ваша бизнес-логика. Я этого не сделаю.
Ответ 21
Есть еще одна возможность рассмотреть.
Современные компиляторы (ahem g++) настолько умны, что они просматривают ваш код, чтобы увидеть, какие инструкции влияют на состояние, а что нет, и если инструкция гарантированно НЕ влияет на состояние, g++ просто удалит эту инструкцию.
Итак, вот что будет. g++ обязательно увидит, что вы читаете, выполняете арифметику, сохраняете, что по сути является значением для мусора, которое производит больше мусора. Поскольку нет никакой гарантии, что новый мусор более полезен, чем старый, он просто избавится от вашей петли. Bloop!
Этот метод полезен, но вот что я буду делать. Объедините UB (Undefined Поведение) с частотой rand().
Конечно, уменьшите rand()
, но смешайте их, чтобы компилятор не делал ничего, что вам не нужно.
И я не убью тебя.
Ответ 22
Использование неинициализированных данных для случайности не обязательно является плохим, если все сделано правильно. Фактически, OpenSSL делает именно это, чтобы засеять его PRNG.
По-видимому, это использование не было хорошо документировано, потому что кто-то заметил, что Valgrind жалуется на использование неинициализированных данных и "фиксирует" его, вызывая ошибку в PRNG.
Итак, вы можете это сделать, но вам нужно знать, что вы делаете, и убедиться, что кто-то, читающий ваш код, понимает это.