Skip to content

Commit 0660799

Browse files
committed
Improve array_rand distribution
1 parent b21de28 commit 0660799

File tree

2 files changed

+58
-30
lines changed

2 files changed

+58
-30
lines changed

ext/standard/array.c

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
#define INTERSECT_COMP_DATA_USER 1
8080
#define INTERSECT_COMP_KEY_INTERNAL 0
8181
#define INTERSECT_COMP_KEY_USER 1
82+
83+
// For array_rand even distribution
84+
#define BITFIELD_BYTES(bits) ((bits + 7) >> 3)
85+
#define BITFIELD_SETBIT(field, bit) field[bit >> 3] |= 1 << (bit & 7)
86+
#define BITFIELD_BITSET(field, bit) ((field[bit >> 3] & (1 << (bit & 7))) != 0)
87+
8288
/* }}} */
8389

8490
ZEND_DECLARE_MODULE_GLOBALS(array)
@@ -5033,56 +5039,76 @@ PHP_FUNCTION(array_rand)
50335039
{
50345040
zval *input;
50355041
zend_long randval, num_req = 1;
5036-
int num_avail;
50375042
zend_string *string_key;
50385043
zend_ulong num_key;
5044+
int i;
5045+
int num_avail;
5046+
char *bitfield;
5047+
int negative_bitfield = 0;
50395048

50405049
if (zend_parse_parameters(ZEND_NUM_ARGS(), "a|l", &input, &num_req) == FAILURE) {
50415050
return;
50425051
}
50435052

50445053
num_avail = zend_hash_num_elements(Z_ARRVAL_P(input));
50455054

5046-
if (ZEND_NUM_ARGS() > 1) {
5047-
if (num_req <= 0 || num_req > num_avail) {
5048-
php_error_docref(NULL, E_WARNING, "Second argument has to be between 1 and the number of elements in the array");
5049-
return;
5050-
}
5051-
}
5052-
5053-
/* Make the return value an array only if we need to pass back more than one result. */
5054-
if (num_req > 1) {
5055-
array_init_size(return_value, (uint32_t)num_req);
5055+
if (num_avail == 0) {
5056+
php_error_docref(NULL, E_WARNING, "Array is empty");
5057+
return;
50565058
}
50575059

5058-
/* We can't use zend_hash_index_find() because the array may have string keys or gaps. */
5059-
ZEND_HASH_FOREACH_KEY(Z_ARRVAL_P(input), num_key, string_key) {
5060-
if (!num_req) {
5061-
break;
5062-
}
5063-
5064-
randval = php_rand();
5060+
if (num_req == 1) {
5061+
randval = php_mt_rand_range(0, num_avail - 1);
50655062

5066-
if ((double) (randval / PHP_RAND_MAX) <= (double) num_req / (double) num_avail) {
5067-
/* If we are returning a single result, just do it. */
5068-
if (Z_TYPE_P(return_value) != IS_ARRAY) {
5063+
ZEND_HASH_FOREACH_KEY(Z_ARRVAL_P(input), num_key, string_key) {
5064+
if (randval-- == 0) {
50695065
if (string_key) {
50705066
RETURN_STR_COPY(string_key);
50715067
} else {
50725068
RETURN_LONG(num_key);
50735069
}
5070+
}
5071+
} ZEND_HASH_FOREACH_END();
5072+
}
5073+
5074+
if (num_req <= 0 || num_req > num_avail) {
5075+
php_error_docref(NULL, E_WARNING, "Second argument has to be between 1 and the number of elements in the array");
5076+
return;
5077+
}
5078+
5079+
/* Make the return value an array only if we need to pass back more than one result. */
5080+
array_init_size(return_value, (uint32_t)num_req);
5081+
if (num_req > (num_avail >> 1)) {
5082+
negative_bitfield = 1;
5083+
num_req = num_avail - num_req;
5084+
}
5085+
5086+
bitfield = emalloc(BITFIELD_BYTES(num_avail));
5087+
memset(bitfield, 0, BITFIELD_BYTES(num_avail));
5088+
5089+
i = num_req;
5090+
while (i) {
5091+
randval = php_mt_rand_range(0, num_avail - 1);
5092+
if (!BITFIELD_BITSET(bitfield, randval)) {
5093+
BITFIELD_SETBIT(bitfield, randval);
5094+
i--;
5095+
}
5096+
}
5097+
5098+
/* We can't use zend_hash_index_find() because the array may have string keys or gaps. */
5099+
i = 0;
5100+
ZEND_HASH_FOREACH_KEY(Z_ARRVAL_P(input), num_key, string_key) {
5101+
if (BITFIELD_BITSET(bitfield, i) ^ negative_bitfield) {
5102+
if (string_key) {
5103+
add_next_index_str(return_value, zend_string_copy(string_key));
50745104
} else {
5075-
/* Append the result to the return value. */
5076-
if (string_key) {
5077-
add_next_index_str(return_value, zend_string_copy(string_key));
5078-
} else {
5079-
add_next_index_long(return_value, num_key);
5080-
}
5105+
add_next_index_long(return_value, num_key);
50815106
}
5082-
num_req--;
50835107
}
5084-
num_avail--;
5108+
i++;
50855109
} ZEND_HASH_FOREACH_END();
5110+
5111+
efree(bitfield);
50865112
}
50875113
/* }}} */
50885114

ext/standard/tests/array/array_rand.phpt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ echo "Done\n";
1818
--EXPECTF--
1919
Warning: array_rand() expects at least 1 parameter, 0 given in %s on line %d
2020
NULL
21+
22+
Warning: array_rand(): Array is empty in %s on line %d
2123
NULL
2224

23-
Warning: array_rand(): Second argument has to be between 1 and the number of elements in the array in %s on line %d
25+
Warning: array_rand(): Array is empty in %s on line %d
2426
NULL
2527

2628
Warning: array_rand() expects parameter 1 to be array, integer given in %s on line %d

0 commit comments

Comments
 (0)