Skip to content

Commit 283c2e8

Browse files
authored
Allow strings to be directly copied between threads in some cases (#93)
closes #71 This works by allowing threads direct access to the original thread's string for as long as it is cached in the origin thread's ThreadedBase connection. As long as the connection lives long enough for the reading thread(s) to dereference the string in question (which, thanks to 4ddf79e, will be the case in all common cases, including dead threads and completed worker tasks), the extra malloc and string copy is avoided on the writer thread, which significantly improves performance in synthetic benchmarks. If the connection is destroyed, it will create persistent copies of any cached strings during free_obj, and the old double-copy method will be used to enable the string to be accessed. However, this is rarely needed. The caveat to this is that pthreads_store_sync_local_properties() will do work more often when strings are used, but I don't think this is a big concern. For most cases, the property table should be small enough for this to not be a problem anyway, and for the large cases, we need to implement dedicated queue data structures anyway. Profiling anyway suggested that the overhead of zend_hash_internal_pointer_reset_ex() was several orders of magnitude bigger a problem anyway (see #42).
1 parent 60babc5 commit 283c2e8

File tree

5 files changed

+156
-10
lines changed

5 files changed

+156
-10
lines changed

Diff for: src/object.c

+2
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ void pthreads_base_free(zend_object *object) {
415415
if (pthreads_globals_lock()) {
416416
if (--base->ts_obj->refcount == 0) {
417417
pthreads_ts_object_free(base);
418+
} else {
419+
pthreads_store_persist_local_properties(object);
418420
}
419421
pthreads_globals_object_delete(base);
420422
pthreads_globals_unlock();

Diff for: src/store.c

+90-10
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ void pthreads_store_sync_local_properties(zend_object* object) { /* {{{ */
9797
remove = 0;
9898
}
9999
#endif
100+
} else if (ts_val->type == STORE_TYPE_STRING_PTR && Z_TYPE_P(val) == IS_STRING) {
101+
pthreads_string_storage_t* string = (pthreads_string_storage_t*)ts_val->data;
102+
if (string->owner.ls == TSRMLS_CACHE && string->string == Z_STR_P(val)) {
103+
//local caching of this by other threads is probably fine too, but fully caching it would probably
104+
//require bytewise comparison, which isn't gonna be very performant
105+
//it should be sufficient to only persist the owner thread's ref, since that's where copies will be
106+
//made from anyway.
107+
remove = 0;
108+
}
100109
}
101110
}
102111

@@ -115,7 +124,7 @@ void pthreads_store_sync_local_properties(zend_object* object) { /* {{{ */
115124
} /* }}} */
116125

117126
static inline zend_bool pthreads_store_retain_in_local_cache(zval* val) {
118-
return IS_PTHREADS_OBJECT(val) || IS_PTHREADS_CLOSURE_OBJECT(val) || IS_EXT_SOCKETS_OBJECT(val);
127+
return IS_PTHREADS_OBJECT(val) || IS_PTHREADS_CLOSURE_OBJECT(val) || IS_EXT_SOCKETS_OBJECT(val) || Z_TYPE_P(val) == IS_STRING;
119128
}
120129

121130
static inline zend_bool pthreads_store_valid_local_cache_item(zval* val) {
@@ -127,7 +136,7 @@ static inline zend_bool pthreads_store_valid_local_cache_item(zval* val) {
127136
/* {{{ */
128137
static inline zend_bool pthreads_store_storage_is_cacheable(zval* zstorage) {
129138
pthreads_storage* storage = TRY_PTHREADS_STORAGE_PTR_P(zstorage);
130-
return storage && (storage->type == STORE_TYPE_PTHREADS || storage->type == STORE_TYPE_CLOSURE || storage->type == STORE_TYPE_SOCKET);
139+
return storage && (storage->type == STORE_TYPE_PTHREADS || storage->type == STORE_TYPE_CLOSURE || storage->type == STORE_TYPE_SOCKET || storage->type == STORE_TYPE_STRING_PTR);
131140
} /* }}} */
132141

133142
/* {{{ Syncs all the cacheable properties from TS storage into local cache */
@@ -314,6 +323,15 @@ zend_bool pthreads_store_isset(zend_object *object, zval *key, int has_set_exist
314323
isset = 0;
315324
}
316325
break;
326+
case IS_PTR: {
327+
pthreads_storage* storage = TRY_PTHREADS_STORAGE_PTR_P(zstorage);
328+
if (storage->type == STORE_TYPE_STRING_PTR) {
329+
pthreads_string_storage_t* string = (pthreads_string_storage_t*)storage->data;
330+
if (ZSTR_LEN(string->string) == 0 || ZSTR_VAL(string->string)[0] == '0') {
331+
isset = 0;
332+
}
333+
}
334+
} break;
317335
default:
318336
break;
319337
}
@@ -350,7 +368,7 @@ static inline void pthreads_store_update_local_property(zend_object* object, zva
350368
zend_hash_update(object->properties, str_key, value);
351369
}
352370
}
353-
Z_ADDREF_P(value);
371+
Z_TRY_ADDREF_P(value);
354372
}
355373
}
356374

@@ -768,6 +786,34 @@ void pthreads_store_tohash(zend_object *object, HashTable *hash) {
768786
}
769787
} /* }}} */
770788

789+
/* {{{ */
790+
void pthreads_store_persist_local_properties(zend_object* object) {
791+
pthreads_zend_object_t* threaded = PTHREADS_FETCH_FROM(object);
792+
pthreads_object_t* ts_obj = threaded->ts_obj;
793+
794+
if (pthreads_monitor_lock(ts_obj->monitor)) {
795+
zval *zstorage;
796+
ZEND_HASH_FOREACH_VAL(&ts_obj->props->hash, zstorage) {
797+
pthreads_storage* storage = TRY_PTHREADS_STORAGE_PTR_P(zstorage);
798+
799+
if (storage != NULL && storage->type == STORE_TYPE_STRING_PTR) {
800+
pthreads_string_storage_t* string = (pthreads_string_storage_t*)storage->data;
801+
if (string->owner.ls == TSRMLS_CACHE) {
802+
//we can't guarantee this string will continue to be available once we stop referencing it on this thread,
803+
//so we must create a persistent copy now
804+
805+
zend_string* persistent_string = pthreads_store_save_string(string->string);
806+
pthreads_store_storage_dtor(zstorage);
807+
808+
ZVAL_STR(zstorage, persistent_string);
809+
}
810+
}
811+
} ZEND_HASH_FOREACH_END();
812+
813+
pthreads_monitor_unlock(ts_obj->monitor);
814+
}
815+
} /* }}} */
816+
771817
/* {{{ */
772818
void pthreads_store_free(pthreads_store_t *store){
773819
zend_hash_destroy(&store->hash);
@@ -787,6 +833,14 @@ static pthreads_storage* pthreads_store_create(pthreads_ident_t* source, zval *u
787833

788834

789835
switch(Z_TYPE_P(unstore)){
836+
case IS_STRING: {
837+
storage->type = STORE_TYPE_STRING_PTR;
838+
pthreads_string_storage_t* string = malloc(sizeof(pthreads_string_storage_t));
839+
string->owner = *source;
840+
string->string = Z_STR_P(unstore);
841+
storage->data = string;
842+
} break;
843+
790844
case IS_RESOURCE: {
791845
pthreads_resource resource = malloc(sizeof(*resource));
792846
storage->type = STORE_TYPE_RESOURCE;
@@ -885,9 +939,14 @@ static zend_result pthreads_store_save_zval(pthreads_ident_t* source, zval *zsto
885939
result = SUCCESS;
886940
break;
887941
case IS_STRING:
888-
ZVAL_STR(zstorage, pthreads_store_save_string(Z_STR_P(write)));
889-
result = SUCCESS;
890-
break;
942+
//permanent strings can be used directly
943+
//non-permanent strings are handled differently
944+
if (GC_FLAGS(Z_STR_P(write)) & IS_STR_PERMANENT) {
945+
ZVAL_STR(zstorage, Z_STR_P(write));
946+
947+
result = SUCCESS;
948+
break;
949+
}
891950
default: {
892951
pthreads_storage *storage = pthreads_store_create(source, write);
893952
if (storage != NULL) {
@@ -906,6 +965,17 @@ static int pthreads_store_convert(pthreads_storage *storage, zval *pzval){
906965
int result = SUCCESS;
907966

908967
switch(storage->type) {
968+
case STORE_TYPE_STRING_PTR: {
969+
pthreads_string_storage_t* string = (pthreads_string_storage_t*)storage->data;
970+
971+
if (string->owner.ls == TSRMLS_CACHE) {
972+
//this thread owns the string - we can use it directly
973+
ZVAL_STR_COPY(pzval, string->string);
974+
} else {
975+
//this thread does not own the string - create a copy
976+
ZVAL_STR(pzval, pthreads_store_restore_string(string->string));
977+
}
978+
} break;
909979
case STORE_TYPE_RESOURCE: {
910980
pthreads_resource stored = (pthreads_resource) storage->data;
911981

@@ -1036,6 +1106,7 @@ static void pthreads_store_restore_zval_ex(zval *unstore, zval *zstorage, zend_b
10361106
ZVAL_COPY(unstore, zstorage);
10371107
break;
10381108
case IS_STRING:
1109+
/* permanent interned string, or persisted string from a dead thread */
10391110
ZVAL_STR(unstore, pthreads_store_restore_string(Z_STR_P(zstorage)));
10401111
break;
10411112
case IS_PTR:
@@ -1061,13 +1132,21 @@ static void pthreads_store_restore_zval(zval *unstore, zval *zstorage) {
10611132
static void pthreads_store_hard_copy_storage(zval *new_zstorage, zval *zstorage) {
10621133
if (Z_TYPE_P(zstorage) == IS_PTR) {
10631134
pthreads_storage *storage = (pthreads_storage *) Z_PTR_P(zstorage);
1064-
pthreads_storage *copy = malloc(sizeof(pthreads_storage));
1135+
if (storage->type == STORE_TYPE_STRING_PTR) {
1136+
//hard-copy string ptrs here, since the destination object might not exist on the thread which owns the string
1137+
//this means that the owning thread may not be aware that this new ref now exists and won't persist the string when it dies
1138+
pthreads_string_storage_t* string = (pthreads_string_storage_t*)storage->data;
1139+
ZVAL_STR(new_zstorage, pthreads_store_save_string(string->string));
1140+
1141+
} else {
1142+
pthreads_storage* copy = malloc(sizeof(pthreads_storage));
10651143

1066-
memcpy(copy, storage, sizeof(pthreads_storage));
1144+
memcpy(copy, storage, sizeof(pthreads_storage));
10671145

1068-
//if we add new store types, their internal data might need to be copied here
1146+
//if we add new store types, their internal data might need to be copied here
10691147

1070-
ZVAL_PTR(new_zstorage, copy);
1148+
ZVAL_PTR(new_zstorage, copy);
1149+
}
10711150
} else if (Z_TYPE_P(zstorage) == IS_STRING) {
10721151
ZVAL_STR(new_zstorage, pthreads_store_save_string(Z_STR_P(zstorage)));
10731152
} else {
@@ -1200,6 +1279,7 @@ static void pthreads_store_storage_dtor (zval *zstorage){
12001279
if (Z_TYPE_P(zstorage) == IS_PTR) {
12011280
pthreads_storage *storage = (pthreads_storage *) Z_PTR_P(zstorage);
12021281
switch (storage->type) {
1282+
case STORE_TYPE_STRING_PTR:
12031283
case STORE_TYPE_RESOURCE:
12041284
case STORE_TYPE_SOCKET:
12051285
case STORE_TYPE_CLOSURE:

Diff for: src/store.h

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ int pthreads_store_shift(zend_object *object, zval *member);
5757
int pthreads_store_chunk(zend_object *object, zend_long size, zend_bool preserve, zval *chunk);
5858
int pthreads_store_pop(zend_object *object, zval *member);
5959
int pthreads_store_count(zend_object *object, zend_long *count);
60+
/* {{{ Copies any thread-local data to permanent storage when an object ref is destroyed */
61+
void pthreads_store_persist_local_properties(zend_object* object); /* }}} */
62+
6063
void pthreads_store_free(pthreads_store_t *store);
6164

6265
/* {{{ * iteration helpers */

Diff for: src/store_types.h

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ typedef enum _pthreads_store_type {
2828
STORE_TYPE_RESOURCE,
2929
STORE_TYPE_SOCKET,
3030
STORE_TYPE_ENUM,
31+
STORE_TYPE_STRING_PTR,
3132
} pthreads_store_type;
3233

3334
typedef struct _pthreads_storage {
@@ -45,4 +46,8 @@ typedef struct _pthreads_closure_storage_t {
4546
pthreads_ident_t owner;
4647
} pthreads_closure_storage_t;
4748

49+
typedef struct _pthreads_string_storage_t {
50+
zend_string* string;
51+
pthreads_ident_t owner;
52+
} pthreads_string_storage_t;
4853
#endif

Diff for: tests/single-copy-strings-basic.phpt

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
--TEST--
2+
Test that string copying works correctly from live and dead threads
3+
--DESCRIPTION--
4+
We implement some optimisations to allow strings to be copied only 1 time, as long as they live on the child thread for long enough for the parent thread to dereference them.
5+
This test verifies the basic functionality, with a string that will be single-copied (the "a" string) and another which will be copied the old way with just-in-time rescue when the object is destroyed.
6+
--FILE--
7+
<?php
8+
9+
$thread = new class extends \Thread{
10+
11+
public \ThreadedArray $buffer;
12+
13+
public function __construct(){
14+
$this->buffer = new \ThreadedArray();
15+
}
16+
17+
public ?string $str = null;
18+
public bool $shutdown = false;
19+
20+
21+
public function run() : void{
22+
$this->synchronized(function() : void{
23+
$this->buffer[] = str_repeat("a", 20);
24+
$this->buffer[] = str_repeat("b", 20);
25+
$this->notify();
26+
});
27+
$this->synchronized(function() : void{
28+
while(!$this->shutdown){
29+
$this->wait();
30+
}
31+
});
32+
}
33+
};
34+
$thread->start();
35+
36+
$thread->synchronized(function() use ($thread) : void{
37+
while($thread->buffer->count() === 0){
38+
$thread->wait();
39+
}
40+
});
41+
var_dump($thread->buffer->shift());
42+
$thread->synchronized(function() use ($thread) : void{
43+
$thread->shutdown = true;
44+
$thread->notify();
45+
});
46+
47+
$thread->join();
48+
var_dump($thread->buffer->shift());
49+
50+
echo "OK\n";
51+
?>
52+
--EXPECT--
53+
string(20) "aaaaaaaaaaaaaaaaaaaa"
54+
string(20) "bbbbbbbbbbbbbbbbbbbb"
55+
OK
56+

0 commit comments

Comments
 (0)