random: Use branchless implementation for mask generation in Randomizer::getBytesFromString() (#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()
This commit is contained in:
Tim Düsterhus 2023-02-08 09:36:12 +01:00 committed by GitHub
parent eabb9b7dea
commit 0cfc45b667
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 23 deletions

View file

@ -388,6 +388,7 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
ZEND_PARSE_PARAMETERS_END();
const size_t source_length = ZSTR_LEN(source);
const size_t max_offset = source_length - 1;
if (source_length < 1) {
zend_argument_value_error(1, "cannot be empty");
@ -401,9 +402,9 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
retval = zend_string_alloc(length, 0);
if (source_length > 0x100) {
if (max_offset > 0xff) {
while (total_size < length) {
uint64_t offset = randomizer->algo->range(randomizer->status, 0, source_length - 1);
uint64_t offset = randomizer->algo->range(randomizer->status, 0, max_offset);
if (EG(exception)) {
zend_string_free(retval);
@ -413,26 +414,14 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
}
} else {
uint64_t mask;
if (source_length <= 0x1) {
mask = 0x0;
} else if (source_length <= 0x2) {
mask = 0x1;
} else if (source_length <= 0x4) {
mask = 0x3;
} else if (source_length <= 0x8) {
mask = 0x7;
} else if (source_length <= 0x10) {
mask = 0xF;
} else if (source_length <= 0x20) {
mask = 0x1F;
} else if (source_length <= 0x40) {
mask = 0x3F;
} else if (source_length <= 0x80) {
mask = 0x7F;
} else {
mask = 0xFF;
}
uint64_t mask = max_offset;
// Copy the top-most bit into all lower bits.
// Shifting by 4 is sufficient, because max_offset
// is guaranteed to fit in an 8-bit integer at this
// point.
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
int failures = 0;
while (total_size < length) {
@ -445,7 +434,7 @@ PHP_METHOD(Random_Randomizer, getBytesFromString)
for (size_t i = 0; i < randomizer->status->last_generated_size; i++) {
uint64_t offset = (result >> (i * 8)) & mask;
if (offset >= source_length) {
if (offset > max_offset) {
if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
zend_string_free(retval);
zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);