Skip to content

Commit 785bbde

Browse files
committed
Optimize transitive comparison helpers
Implement scalar/string fast paths and enum/object ordering rules so both regular and transitive SORT_REGULAR comparators skip redundant zend_compare() calls. Introduce php_array_compare_non_numeric_strings() and dedicated transitive string/number helpers to minimize numeric parsing.
1 parent 773e90c commit 785bbde

File tree

1 file changed

+126
-20
lines changed

1 file changed

+126
-20
lines changed

ext/standard/array.c

Lines changed: 126 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ static zend_never_inline ZEND_COLD int stable_sort_fallback(Bucket *a, Bucket *b
109109
static int php_array_compare_transitive(zval *op1, zval *op2);
110110

111111
static int php_array_compare_transitive(zval *op1, zval *op2);
112+
static zend_always_inline int php_array_compare_non_numeric_strings(zend_string *s1, zend_string *s2)
113+
{
114+
size_t min_len = s1->len < s2->len ? s1->len : s2->len;
115+
int cmp = memcmp(s1->val, s2->val, min_len);
116+
if (cmp != 0) {
117+
return cmp < 0 ? -1 : 1;
118+
}
119+
return ZEND_THREEWAY_COMPARE(s1->len, s2->len);
120+
}
121+
112122
/* Generate inlined unstable and stable variants, and non-inlined reversed variants. */
113123
#define DEFINE_SORT_VARIANTS(name) \
114124
static zend_never_inline int php_array_##name##_unstable(Bucket *a, Bucket *b) { \
@@ -132,6 +142,10 @@ static zend_always_inline int php_array_key_compare_impl(Bucket *f, Bucket *s) /
132142
if (f->key == NULL && s->key == NULL) {
133143
return (zend_long)f->h > (zend_long)s->h ? 1 : -1;
134144
} else if (f->key && s->key) {
145+
if ((unsigned char)f->key->val[0] > '9'
146+
&& (unsigned char)s->key->val[0] > '9') {
147+
return php_array_compare_non_numeric_strings(f->key, s->key);
148+
}
135149
return zendi_smart_strcmp(f->key, s->key);
136150
}
137151
if (f->key) {
@@ -296,26 +310,91 @@ static zend_always_inline int php_array_key_compare_string_locale_unstable_i(Buc
296310

297311
static zend_always_inline int php_array_data_compare_impl(Bucket *f, Bucket *s) /* {{{ */
298312
{
299-
int result = zend_compare(&f->val, &s->val);
300-
/* Special enums handling for array_unique. We don't want to add this logic to zend_compare as
301-
* that would be observable via comparison operators. */
302-
zval *rhs = &s->val;
303-
ZVAL_DEREF(rhs);
304-
if (UNEXPECTED(Z_TYPE_P(rhs) == IS_OBJECT)
305-
&& result == ZEND_UNCOMPARABLE
306-
&& (Z_OBJCE_P(rhs)->ce_flags & ZEND_ACC_ENUM)) {
307-
zval *lhs = &f->val;
308-
ZVAL_DEREF(lhs);
309-
if (Z_TYPE_P(lhs) == IS_OBJECT && (Z_OBJCE_P(lhs)->ce_flags & ZEND_ACC_ENUM)) {
310-
// Order doesn't matter, we just need to group the same enum values
311-
uintptr_t lhs_uintptr = (uintptr_t)Z_OBJ_P(lhs);
312-
uintptr_t rhs_uintptr = (uintptr_t)Z_OBJ_P(rhs);
313+
zval *op1 = &f->val;
314+
zval *op2 = &s->val;
315+
316+
if (EXPECTED(Z_TYPE_P(op1) == IS_LONG && Z_TYPE_P(op2) == IS_LONG)) {
317+
return ZEND_THREEWAY_COMPARE(Z_LVAL_P(op1), Z_LVAL_P(op2));
318+
}
319+
320+
if (EXPECTED(Z_TYPE_P(op1) == IS_DOUBLE && Z_TYPE_P(op2) == IS_DOUBLE)) {
321+
return ZEND_THREEWAY_COMPARE(Z_DVAL_P(op1), Z_DVAL_P(op2));
322+
}
323+
324+
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING && Z_TYPE_P(op2) == IS_STRING)) {
325+
zend_string *str1 = Z_STR_P(op1);
326+
zend_string *str2 = Z_STR_P(op2);
327+
328+
if ((unsigned char)str1->val[0] > '9'
329+
&& (unsigned char)str2->val[0] > '9') {
330+
return php_array_compare_non_numeric_strings(str1, str2);
331+
}
332+
333+
return zendi_smart_strcmp(str1, str2);
334+
}
335+
336+
ZVAL_DEREF(op1);
337+
ZVAL_DEREF(op2);
338+
339+
if (Z_TYPE_P(op1) == IS_OBJECT && Z_TYPE_P(op2) == IS_OBJECT) {
340+
if (Z_OBJ_P(op1) == Z_OBJ_P(op2)) {
341+
return 0;
342+
}
343+
344+
if (Z_OBJCE_P(op1) != Z_OBJCE_P(op2)) {
345+
bool lhs_enum = (Z_OBJCE_P(op1)->ce_flags & ZEND_ACC_ENUM) != 0;
346+
bool rhs_enum = (Z_OBJCE_P(op2)->ce_flags & ZEND_ACC_ENUM) != 0;
347+
348+
if (UNEXPECTED(lhs_enum || rhs_enum)) {
349+
if (lhs_enum && !rhs_enum) {
350+
return 1;
351+
} else if (!lhs_enum && rhs_enum) {
352+
return -1;
353+
}
354+
uintptr_t lhs_uintptr = (uintptr_t)Z_OBJ_P(op1);
355+
uintptr_t rhs_uintptr = (uintptr_t)Z_OBJ_P(op2);
356+
return lhs_uintptr == rhs_uintptr ? 0 : (lhs_uintptr < rhs_uintptr ? -1 : 1);
357+
}
358+
359+
return ZEND_UNCOMPARABLE;
360+
}
361+
362+
if (UNEXPECTED(Z_OBJCE_P(op1)->ce_flags & ZEND_ACC_ENUM)) {
363+
uintptr_t lhs_uintptr = (uintptr_t)Z_OBJ_P(op1);
364+
uintptr_t rhs_uintptr = (uintptr_t)Z_OBJ_P(op2);
313365
return lhs_uintptr == rhs_uintptr ? 0 : (lhs_uintptr < rhs_uintptr ? -1 : 1);
314-
} else {
315-
// Shift enums to the end of the array
316-
return -1;
317366
}
367+
368+
return zend_compare(op1, op2);
369+
}
370+
371+
if (Z_TYPE_P(op1) == IS_ARRAY && Z_TYPE_P(op2) == IS_ARRAY) {
372+
if (Z_ARR_P(op1) == Z_ARR_P(op2)) {
373+
return 0;
374+
}
375+
376+
uint32_t n1 = zend_hash_num_elements(Z_ARRVAL_P(op1));
377+
uint32_t n2 = zend_hash_num_elements(Z_ARRVAL_P(op2));
378+
if (n1 != n2) {
379+
return ZEND_THREEWAY_COMPARE(n1, n2);
380+
}
381+
382+
return zend_compare(op1, op2);
318383
}
384+
385+
int result = zend_compare(op1, op2);
386+
if (UNEXPECTED(result == ZEND_UNCOMPARABLE
387+
&& Z_TYPE_P(op2) == IS_OBJECT
388+
&& (Z_OBJCE_P(op2)->ce_flags & ZEND_ACC_ENUM))) {
389+
if (Z_TYPE_P(op1) == IS_OBJECT && (Z_OBJCE_P(op1)->ce_flags & ZEND_ACC_ENUM)) {
390+
uintptr_t lhs_uintptr = (uintptr_t)Z_OBJ_P(op1);
391+
uintptr_t rhs_uintptr = (uintptr_t)Z_OBJ_P(op2);
392+
return lhs_uintptr == rhs_uintptr ? 0 : (lhs_uintptr < rhs_uintptr ? -1 : 1);
393+
}
394+
/* Shift enums to the end of the array. */
395+
return -1;
396+
}
397+
319398
return result;
320399
}
321400
/* }}} */
@@ -729,6 +808,10 @@ static zend_always_inline int php_array_key_compare_regular_unstable_i(Bucket *f
729808
if (f->key == NULL && s->key == NULL) {
730809
return (zend_long)f->h > (zend_long)s->h ? 1 : -1;
731810
} else if (f->key && s->key) {
811+
if ((unsigned char)f->key->val[0] > '9'
812+
&& (unsigned char)s->key->val[0] > '9') {
813+
return php_array_compare_non_numeric_strings(f->key, s->key);
814+
}
732815
return php_array_smart_strcmp_transitive(f->key, s->key);
733816
}
734817
if (f->key) {
@@ -746,13 +829,36 @@ static zend_always_inline int php_array_key_compare_regular_unstable_i(Bucket *f
746829

747830
static zend_always_inline int php_array_data_compare_regular_unstable_i(Bucket *f, Bucket *s)
748831
{
749-
int result = php_array_compare_regular(&f->val, &s->val);
750-
zval *rhs = &s->val;
832+
zval *op1 = &f->val;
833+
zval *op2 = &s->val;
834+
835+
if (EXPECTED(Z_TYPE_P(op1) == IS_LONG && Z_TYPE_P(op2) == IS_LONG)) {
836+
return ZEND_THREEWAY_COMPARE(Z_LVAL_P(op1), Z_LVAL_P(op2));
837+
}
838+
839+
if (EXPECTED(Z_TYPE_P(op1) == IS_DOUBLE && Z_TYPE_P(op2) == IS_DOUBLE)) {
840+
return ZEND_THREEWAY_COMPARE(Z_DVAL_P(op1), Z_DVAL_P(op2));
841+
}
842+
843+
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING && Z_TYPE_P(op2) == IS_STRING)) {
844+
zend_string *str1 = Z_STR_P(op1);
845+
zend_string *str2 = Z_STR_P(op2);
846+
847+
if ((unsigned char)str1->val[0] > '9'
848+
&& (unsigned char)str2->val[0] > '9') {
849+
return php_array_compare_non_numeric_strings(str1, str2);
850+
}
851+
852+
return php_array_smart_strcmp_transitive(str1, str2);
853+
}
854+
855+
int result = php_array_compare_regular(op1, op2);
856+
zval *rhs = op2;
751857
ZVAL_DEREF(rhs);
752858
if (UNEXPECTED(Z_TYPE_P(rhs) == IS_OBJECT)
753859
&& result == ZEND_UNCOMPARABLE
754860
&& (Z_OBJCE_P(rhs)->ce_flags & ZEND_ACC_ENUM)) {
755-
zval *lhs = &f->val;
861+
zval *lhs = op1;
756862
ZVAL_DEREF(lhs);
757863
if (Z_TYPE_P(lhs) == IS_OBJECT && (Z_OBJCE_P(lhs)->ce_flags & ZEND_ACC_ENUM)) {
758864
uintptr_t lhs_uintptr = (uintptr_t)Z_OBJ_P(lhs);

0 commit comments

Comments
 (0)