Skip to content

Commit 0cfc45b

Browse files
authored
random: Use branchless implementation for mask generation in Randomizer::getBytesFromString() (php#10522)
* random: Add `max_offset` local to Randomizer::getBytesFromString() * random: Use branchless implementation for mask generation in Randomizer::getBytesFromString() This was benchmarked against clzl with a standalone script with random inputs and is slightly faster. clzl requires an additional branch to handle the source_length = 1 / max_offset = 0 case. * Improve comment for masking in Randomizer::getBytesFromString()
1 parent eabb9b7 commit 0cfc45b

File tree

2 files changed

+62
-23
lines changed

2 files changed

+62
-23
lines changed

Diff for: ext/random/randomizer.c

+12-23
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
388388
ZEND_PARSE_PARAMETERS_END();
389389

390390
const size_t source_length = ZSTR_LEN(source);
391+
const size_t max_offset = source_length - 1;
391392

392393
if (source_length < 1) {
393394
zend_argument_value_error(1, "cannot be empty");
@@ -401,9 +402,9 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
401402

402403
retval = zend_string_alloc(length, 0);
403404

404-
if (source_length > 0x100) {
405+
if (max_offset > 0xff) {
405406
while (total_size < length) {
406-
uint64_t offset = randomizer->algo->range(randomizer->status, 0, source_length - 1);
407+
uint64_t offset = randomizer->algo->range(randomizer->status, 0, max_offset);
407408

408409
if (EG(exception)) {
409410
zend_string_free(retval);
@@ -413,26 +414,14 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
413414
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
414415
}
415416
} else {
416-
uint64_t mask;
417-
if (source_length <= 0x1) {
418-
mask = 0x0;
419-
} else if (source_length <= 0x2) {
420-
mask = 0x1;
421-
} else if (source_length <= 0x4) {
422-
mask = 0x3;
423-
} else if (source_length <= 0x8) {
424-
mask = 0x7;
425-
} else if (source_length <= 0x10) {
426-
mask = 0xF;
427-
} else if (source_length <= 0x20) {
428-
mask = 0x1F;
429-
} else if (source_length <= 0x40) {
430-
mask = 0x3F;
431-
} else if (source_length <= 0x80) {
432-
mask = 0x7F;
433-
} else {
434-
mask = 0xFF;
435-
}
417+
uint64_t mask = max_offset;
418+
// Copy the top-most bit into all lower bits.
419+
// Shifting by 4 is sufficient, because max_offset
420+
// is guaranteed to fit in an 8-bit integer at this
421+
// point.
422+
mask |= mask >> 1;
423+
mask |= mask >> 2;
424+
mask |= mask >> 4;
436425

437426
int failures = 0;
438427
while (total_size < length) {
@@ -445,7 +434,7 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
445434
for (size_t i = 0; i < randomizer->status->last_generated_size; i++) {
446435
uint64_t offset = (result >> (i * 8)) & mask;
447436

448-
if (offset >= source_length) {
437+
if (offset > max_offset) {
449438
if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
450439
zend_string_free(retval);
451440
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);

Diff for: ext/random/tests/03_randomizer/methods/getBytesFromString_fast_path.phpt

+50
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,32 @@ for ($i = 1; $i <= strlen($allBytes); $i *= 2) {
4747
echo PHP_EOL;
4848
}
4949

50+
// Test lengths that are one more than the powers of two. For these
51+
// the maximum offset will be a power of two and thus a minimal number
52+
// of bits will be set in the offset.
53+
for ($i = 1; ($i + 1) <= strlen($allBytes); $i *= 2) {
54+
$oneMore = $i + 1;
55+
56+
echo "{$oneMore}:", PHP_EOL;
57+
58+
$wrapper = new TestWrapperEngine($xoshiro);
59+
$r = new Randomizer($wrapper);
60+
$result = $r->getBytesFromString(substr($allBytes, 0, $oneMore), 20000);
61+
62+
$count = [];
63+
for ($j = 0; $j < strlen($result); $j++) {
64+
$b = $result[$j];
65+
$count[ord($b)] ??= 0;
66+
$count[ord($b)]++;
67+
}
68+
69+
// We expect that each possible value appears at least once, if
70+
// not is is very likely that some bits were erroneously masked away.
71+
var_dump(count($count));
72+
73+
echo PHP_EOL;
74+
}
75+
5076
echo "Slow Path:", PHP_EOL;
5177

5278
$wrapper = new TestWrapperEngine($xoshiro);
@@ -107,6 +133,30 @@ int(128)
107133
int(2500)
108134
int(256)
109135

136+
2:
137+
int(2)
138+
139+
3:
140+
int(3)
141+
142+
5:
143+
int(5)
144+
145+
9:
146+
int(9)
147+
148+
17:
149+
int(17)
150+
151+
33:
152+
int(33)
153+
154+
65:
155+
int(65)
156+
157+
129:
158+
int(129)
159+
110160
Slow Path:
111161
int(20000)
112162
int(256)

0 commit comments

Comments
 (0)