random: Optimize Randomizer::getBytes() (#15228)

This patch greatly improves the performance for the common case of using a
64-bit engine and requesting a length that is a multiple of 8.

It does so by providing a fast path that will just `memcpy()` (which will be
optimized out) the returned uint64_t directly into the output buffer,
byteswapping it for big endian architectures.

The existing byte-wise copying logic was mostly left alone. It only received an
optimization of the shifting and masking that was previously applied to
`Randomizer::getBytesFromString()` in 1fc2ddc996.

Co-authored-by: Saki Takamachi <saki@php.net>
This commit is contained in:
Tim Düsterhus 2024-08-05 19:12:29 +02:00 committed by GitHub
parent 2f27e0b2ab
commit 31e2d2b86c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 5 deletions

View file

@ -81,8 +81,8 @@ typedef struct _php_random_status_state_user {
} php_random_status_state_user; } php_random_status_state_user;
typedef struct _php_random_result { typedef struct _php_random_result {
const uint64_t result; uint64_t result;
const size_t size; size_t size;
} php_random_result; } php_random_result;
typedef struct _php_random_algo { typedef struct _php_random_algo {

View file

@ -26,6 +26,7 @@
#include "Zend/zend_enum.h" #include "Zend/zend_enum.h"
#include "Zend/zend_exceptions.h" #include "Zend/zend_exceptions.h"
#include "zend_portability.h"
static inline void randomizer_common_init(php_random_randomizer *randomizer, zend_object *engine_object) { static inline void randomizer_common_init(php_random_randomizer *randomizer, zend_object *engine_object) {
if (engine_object->ce->type == ZEND_INTERNAL_CLASS) { if (engine_object->ce->type == ZEND_INTERNAL_CLASS) {
@ -292,14 +293,48 @@ PHP_METHOD(Random_Randomizer, getBytes)
size_t length = (size_t)user_length; size_t length = (size_t)user_length;
retval = zend_string_alloc(length, 0); retval = zend_string_alloc(length, 0);
while (total_size < length) { php_random_result result;
php_random_result result = engine.algo->generate(engine.state); while (total_size + 8 <= length) {
result = engine.algo->generate(engine.state);
if (EG(exception)) { if (EG(exception)) {
zend_string_free(retval); zend_string_free(retval);
RETURN_THROWS(); RETURN_THROWS();
} }
/* If the result is not 64 bits, we can't use the fast path and
* we don't attempt to use it in the future, because we don't
* expect engines to change their output size.
*
* While it would be possible to always memcpy() the entire output,
* using result.size as the length that would result in much worse
* assembly, because it will actually emit a call to memcpy()
* instead of just storing the 64 bit value at a memory offset.
*/
if (result.size != 8) {
goto non_64;
}
#ifdef WORDS_BIGENDIAN
uint64_t swapped = ZEND_BYTES_SWAP64(result.result);
memcpy(ZSTR_VAL(retval) + total_size, &swapped, 8);
#else
memcpy(ZSTR_VAL(retval) + total_size, &result.result, 8);
#endif
total_size += 8;
}
while (total_size < length) {
result = engine.algo->generate(engine.state);
if (EG(exception)) {
zend_string_free(retval);
RETURN_THROWS();
}
non_64:
for (size_t i = 0; i < result.size; i++) { for (size_t i = 0; i < result.size; i++) {
ZSTR_VAL(retval)[total_size++] = (result.result >> (i * 8)) & 0xff; ZSTR_VAL(retval)[total_size++] = result.result & 0xff;
result.result >>= 8;
if (total_size >= length) { if (total_size >= length) {
break; break;
} }