Странное поведение rand () в Xcode

При использовании Apple Xcode программа C, которую я включаю ниже, всегда будет возвращать 5 для y, но z будет возвращать случайное число, даже если они имеют то же выражение. Можете ли вы определить причину такого поведения?

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main()
{
    int x,y,z;
    srand((unsigned int)time(NULL));
    x = ((unsigned int)time(NULL));
    y=((double)rand()/(double)(RAND_MAX)*10);
    z=((double)rand()/(double)(RAND_MAX)*10);
    printf("Time %d\n", x);
    printf("\n");
    printf("Number y %d\n", y);
    printf("\n");
    printf("Number z %d\n", z);
    printf("\n");
    return 0;
}

person MarkAsh    schedule 22.10.2017    source источник
comment
Это связано с тем, что значение из time (начальное значение) медленно увеличивается и делится на большое число, поэтому частное не будет отличаться до тех пор, пока не пройдет более длительное время. Второй вызов rand производит значение, совершенно отличное от начального.   -  person Weather Vane    schedule 22.10.2017
comment
Кажется, это ограничено Xcode и Mac OS X, поскольку у меня нет этой проблемы при работе в Linux. Это так? А в Xcode, если я использую srandom и random (), он работает одинаково в обоих случаях. Отличаются ли random () и srandom от srand и rand ()?   -  person MarkAsh    schedule 22.10.2017
comment
Я получаю аналогичный результат от MSVC, но 7 вместо 5. Я ожидаю, что 7 в конечном итоге вырастет до 8.   -  person Weather Vane    schedule 22.10.2017
comment
Я тоже получаю 5 на OS X / XCode.   -  person Stephan Lechner    schedule 22.10.2017
comment
На страницах руководства для rand (man 3 rand) указано, что интерфейс rand устарел и его следует заменить на arc4random (3) (например, перейти на терминал и ввести man 3 arc4random, чтобы получить документацию).   -  person James Snook    schedule 22.10.2017
comment
Возможно, это зависит от реализации rand, которая может вычислить новое значение и вернуть его или вернуть текущее значение после вычисления следующего значения. В последнем случае начальное значение будет возвращено при первом вызове rand. P.S. в моем тесте 7 теперь продвинулся до 8.   -  person Weather Vane    schedule 22.10.2017
comment
Вы звоните time() дважды; существует вероятность его изменения между двумя вызовами. Если x предназначен для хранения значения, используемого в качестве начального числа, лучше назначить x, а затем использовать x как начальное значение.   -  person Clifford    schedule 22.10.2017
comment
этот оператор: x = ((unsigned int)time(NULL)); присваивает 'unsigned int' 'int'. Функция неявного преобразования C может вас спасти (но компилятор выдаст предупреждающее сообщение.)   -  person user3629249    schedule 23.10.2017
comment
в 32-битной архитектуре это выражение: (double)(RAND_MAX)*10) пытается умножить 2147483647 (максимальное положительное целое число со знаком) на 10. Это приведет к переполнению числа со знаком. Умножение двойного на целое число приводит к неявному преобразованию, выполняемому компилятором. Это заставит компилятор вывести предупреждающее сообщение. Кроме того, присвоение int из double заставит компилятор вывести предупреждающее сообщение. При компиляции всегда включайте предупреждения, а затем исправляйте эти предупреждения.   -  person user3629249    schedule 23.10.2017
comment
для удобства чтения и понимания: 1) следуйте аксиоме: только один оператор на строку и (максимум) одно объявление переменной на оператор.   -  person user3629249    schedule 23.10.2017


Ответы (4)


rand-31

Вот адаптация вашей программы, в которой более подробно показано, что происходит:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int x = ((unsigned int)time(NULL));
    srand((unsigned int)x);
    int r1 = rand();
    int y  = ((double)r1/(double)(RAND_MAX)*10);
    int r2 = rand();
    int z  = ((double)r2/(double)(RAND_MAX)*10);
    printf("Time %d\n", x);
    printf("Random 1: %d\n", r1);
    printf("Number y: %d\n", y);
    printf("Random 2: %d\n", r2);
    printf("Number z: %d\n", z);
    return 0;
}

Он фиксирует результат двух rand() вызовов независимо от вычислений, чтобы их можно было распечатать. Это также гарантирует, что значение, переданное в time(), совпадает с напечатанным значением, хотя шансы, что они будут разными, довольно малы.

Когда я запустил его несколько раз, я получил следующие результаты:

Time 1508694677
Random 1: 1292016210
Number y: 6
Random 2: 1709286653
Number z: 7

Time 1508694685
Random 1: 1292150666
Number y: 6
Random 2: 1821604998
Number z: 8

Time 1508694701
Random 1: 1292419578
Number y: 6
Random 2: 2046241688
Number z: 9

Time 1508694841
Random 1: 1294772558
Number y: 6
Random 2: 790587255
Number z: 3

Как видите, значения, возвращаемые rand(), разные, но не сильно отличаются, и, следовательно, вычисленное значение не сильно отличается.

rand-23

Один простой вариант обновления - использовать функции drand48(), например:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int x = ((unsigned int)time(NULL));
    srand48((unsigned int)x);
    int r1 = lrand48();
    int y  = ((double)r1/(double)(RAND_MAX)*10);
    int r2 = lrand48();
    int z  = ((double)r2/(double)(RAND_MAX)*10);
    printf("RAND_MAX: %d\n", RAND_MAX);
    printf("Time:     %d\n", x);
    printf("Random 1: %d\n", r1);
    printf("Number y: %d\n", y);
    printf("Random 2: %d\n", r2);
    printf("Number z: %d\n", z);
    return 0;
}

Код печатает RAND_MAX в основном для того, чтобы показать, что он совместим с диапазоном значений, возвращаемым lrand48(), который задокументирован для возврата значения в диапазоне 0..2 31 -1.

Это дает разные и, возможно, лучшие результаты:

RAND_MAX: 2147483647
Time:     1508695069
Random 1: 705270938
Number y: 3
Random 2: 1758232243
Number z: 8

RAND_MAX: 2147483647
Time:     1508695074
Random 1: 1465504939
Number y: 6
Random 2: 733780153
Number z: 3

RAND_MAX: 2147483647
Time:     1508695080
Random 1: 1948289010
Number y: 9
Random 2: 1222424564
Number z: 5

rand-13

В качестве альтернативы вы можете сделать то, что указано в документации macOS, и переключиться на _ 14_. Ссылка ведет на «устаревшую» документацию XCode 5, но функциональность в последнее время не менялась.

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int x = ((unsigned int)time(NULL));
    int r1 = arc4random_uniform(INT_MAX);
    int y  = ((double)r1/(double)(RAND_MAX)*10);
    int r2 = arc4random_uniform(INT_MAX);
    int z  = ((double)r2/(double)(RAND_MAX)*10);
    printf("INT_MAX:  %d\n", INT_MAX);
    printf("RAND_MAX: %d\n", RAND_MAX);
    printf("Time:     %d\n", x);
    printf("Random 1: %d\n", r1);
    printf("Number y: %d\n", y);
    printf("Random 2: %d\n", r2);
    printf("Number z: %d\n", z);
    return 0;
}

Он использует arc4random_uniform() для генерации значения в диапазоне 0..INT_MAX, в том же диапазоне, что и другие сгенерированные функции. Аналога srand() с семейством функций arc4random() не существует.

Примеры прогонов:

INT_MAX:  2147483647
RAND_MAX: 2147483647
Time:     1508695894
Random 1: 938614006
Number y: 4
Random 2: 851262647
Number z: 3

INT_MAX:  2147483647
RAND_MAX: 2147483647
Time:     1508695900
Random 1: 1332829945
Number y: 6
Random 2: 386007903
Number z: 1

INT_MAX:  2147483647
RAND_MAX: 2147483647
Time:     1508695913
Random 1: 1953583540
Number y: 9
Random 2: 1273643162
Number z: 5

rand-83

Обратите внимание: если вам нужно случайное целое число от 0 до 10, вам следует использовать arc4random_uniform(10) напрямую, а не выполнять вычисления.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int x = ((unsigned int)time(NULL));
    int y = arc4random_uniform(10);
    int z = arc4random_uniform(10);
    printf("Time:     %d\n", x);
    printf("Number y: %d\n", y);
    printf("Number z: %d\n", z);
    return 0;
}

Примеры результатов:

Time:     1508696162
Number y: 5
Number z: 3

Time:     1508696163
Number y: 7
Number z: 5

Time:     1508696165
Number y: 3
Number z: 8

Еще одно преимущество arc4random() и др. Проявилось, когда мне удалось запустить программу 3 раза за одну секунду. С другими генераторами они дали бы те же результаты из-за использования того же начального числа.

$ rand-83;rand-83;rand-83
Time:     1508696213
Number y: 4
Number z: 0
Time:     1508696213
Number y: 0
Number z: 1
Time:     1508696213
Number y: 9
Number z: 6
$

$ rand-31;rand-31;rand-31
Time 1508696334
Random 1: 1319865409
Number y: 6
Random 2: 1619339200
Number z: 7
Time 1508696334
Random 1: 1319865409
Number y: 6
Random 2: 1619339200
Number z: 7
Time 1508696334
Random 1: 1319865409
Number y: 6
Random 2: 1619339200
Number z: 7
$

JFTR: код, скомпилированный на MacBook Pro под управлением macOS High Sierra 10.13. Я использовал GCC 7.2.0 (а не XCode 9) для компиляции, но системную библиотеку - и это библиотека, которая здесь критична.

person Jonathan Leffler    schedule 22.10.2017
comment
Я вижу, что это проблема, связанная с Mac GCC (я также использую MacBook Pro с High Sierra 10.13, но с бета-версией Xcode 9.1), проблема в том, что мне нужно разработать максимально возможную совместимость, поскольку мой учитель использует Linux system, и он без проблем использует rand () и srand. - person MarkAsh; 22.10.2017
comment
Нет никакой гарантии, какое качество случайного числа вы получите от функции rand(). Существуют ограниченные гарантии относительно диапазона значений, возвращаемых функцией rand(). Переносимый код не может ожидать одинаковых результатов на разных машинах, использующих функцию rand(). Единственная проблема связана с ожиданиями и / или алгоритмом, используемым для получения значения в диапазоне 0..10 (или 0..9). Есть другие, более эффективные способы сделать это. - person Jonathan Leffler; 22.10.2017
comment
Обратите внимание на ответ Роланда Иллига на вопрос Правильна ли эта реализация перемешивания Фишера-Ятса на языке C?, в котором показано, как сгенерировать число в диапазон 0..N-1 точно без смещения. - person Jonathan Leffler; 22.10.2017

Вы только что усвоили важный урок:

Не используйте time(NULL) для заполнения генератора случайных чисел.

Да, я знаю, что «все так делают». Это не значит, что это правильно. (Фактически, «все» этого не делают, хотя это очень часто встречается в примере кода. Это очень редко - и даже в редких случаях некорректно - в коде, который на самом деле требует случайных чисел для производственных целей.)

Дело в том, что «время в секундах с эпохи» - очень неслучайное число. На изменение первых шести битов уходит целый год; в течение почти дня меняются только младшие 16 бит, а на секунду - что достаточно для запуска тысяч процессов - не меняется вообще. Поскольку вы гарантированно сможете получить такую ​​же случайную последовательность, если используете одно и то же начальное число, должно быть очевидно, что вам нужно немного усерднее работать, чтобы гарантировать, что у вас есть более случайное начальное число.

Так что делать? Большинство операционных систем в наши дни имеют некоторый механизм для генерации действительно случайного числа (или числа, которое настолько близко к случайному, насколько позволяют законы физики). Во многих Unix и Unix-подобных системах, включая Mac OS X и Linux, вы можете получить очень хорошее 32-битное случайное число без знака, прочитав четыре байта из /dev/random. Это слишком дорого для каждого случайного числа, которое вам может понадобиться, но в качестве начального числа это отлично:

uint32_t seed = 0;
FILE* rf = fopen("/dev/random", "r");
if (rf != NULL) {
  if (fread(seed, sizeof seed, 1, rf) != 1) seed = 0;
  fclose(rf);
}
if (!seed) { /* Fallback to some other mechanism */ }
srand(seed);

Независимо от заполнения, rand часто не очень хороший генератор случайных чисел, особенно в реализациях, где RAND_MAX равно 32767. Но никакой генератор случайных чисел, каким бы хорошим он ни был, не может компенсировать отсутствие хорошей стратегии заполнения.

(Некоторые библиотеки заполняются автоматически, но есть веская причина, по которой ручное заполнение является частью интерфейса: вам может потребоваться повторно запустить ту же последовательность случайных чисел. Например, если вы используете ГПСЧ для генерации случайных тестовых данных, а некоторые из них запускают вашей тестовой программы вызывает ошибку, вы захотите повторно запустить программу, используя ту же последовательность случайных чисел, чтобы убедиться, что ошибка была исправлена.Одна простая стратегия - использовать флаг командной строки для предоставления явного начального числа случайных чисел и убедитесь, что тестовая оснастка всегда печатает начальное число случайных чисел, которое привело к результатам.)

person rici    schedule 22.10.2017

после удаления (ненужного) беспорядка из вашей программы следующий код:

  1. чисто компилируется
  2. удаляет ненужные локальные переменные
  3. выполняет желаемую функциональность.
  4. ясно показывает, что на самом деле происходит с функциями time() и rand().
  5. документы, почему включен каждый файл заголовка

а теперь предлагаемый код:

#include <stdio.h>  // printf()
#include <time.h>   // time()
#include <stdlib.h> // srand(), rand()

int main( void )
{
    srand((unsigned int)time(NULL));

    printf("Time %u\n\n", (unsigned int)time(NULL) );

    printf("first call to rand:  %d\n\n", rand());

    printf("second call to rand: %d\n\n", rand());

    return 0;
}

типичный запуск предложенного кода дает выходные данные, подобные следующему:

Time 1508783846

first call to rand:  1113266715

second call to rand: 810898123
person user3629249    schedule 23.10.2017

Мне действительно удалось идентифицировать проблему, и она связана с использованием бета-версии Xcode 9.1, которая по какой-то причине сломала srand и rand (). После возврата к Xcode 9.0.1 все снова работает нормально.

person MarkAsh    schedule 26.10.2017