Heim  >  Artikel  >  Backend-Entwicklung  >  Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

青灯夜游
青灯夜游nach vorne
2021-09-03 19:05:492908Durchsuche

In diesem Artikel lernen Sie die Objekte in PHP 7 und PHP 5 kennen und vergleichen sie, um die Unterschiede zwischen ihnen zu erkennen!

Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

1. Einführung in die Klasse

  Klasse, Schnittstelle, Merkmal in PHP werden alle mit der zend_class_entry-Struktur auf der untersten Ebene implementiert.

struct _zend_class_entry {
	char type;
	const char *name;
	zend_uint name_length;
	struct _zend_class_entry *parent;
	int refcount;
	zend_uint ce_flags;

	HashTable function_table;
	HashTable properties_info;
	zval **default_properties_table;
	zval **default_static_members_table;
	zval **static_members_table;
	HashTable constants_table;
	int default_properties_count;
	int default_static_members_count;

	union _zend_function *constructor;
	union _zend_function *destructor;
	union _zend_function *clone;
	union _zend_function *__get;
	union _zend_function *__set;
	union _zend_function *__unset;
	union _zend_function *__isset;
	union _zend_function *__call;
	union _zend_function *__callstatic;
	union _zend_function *__tostring;
	union _zend_function *serialize_func;
	union _zend_function *unserialize_func;

	zend_class_iterator_funcs iterator_funcs;

	/* handlers */
	zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC);
	zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC);
	int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* a class implements this interface */
	union _zend_function *(*get_static_method)(zend_class_entry *ce, char* method, int method_len TSRMLS_DC);

	/* serializer callbacks */
	int (*serialize)(zval *object, unsigned char **buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC);
	int (*unserialize)(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC);

	zend_class_entry **interfaces;
	zend_uint num_interfaces;
	
	zend_class_entry **traits;
	zend_uint num_traits;
	zend_trait_alias **trait_aliases;
	zend_trait_precedence **trait_precedences;

	union {
		struct {
			const char *filename;
			zend_uint line_start;
			zend_uint line_end;
			const char *doc_comment;
			zend_uint doc_comment_len;
		} user;
		struct {
			const struct _zend_function_entry *builtin_functions;
			struct _zend_module_entry *module;
		} internal;
	} info;
};

  Die zend_class_entry-Struktur enthält eine große Anzahl von Zeigern und Hashtabellen, die die Struktur selbst verursachen Es nimmt viel Speicherplatz in Anspruch. Darüber hinaus müssen die Zeiger in der Struktur den entsprechenden Speicherplatz separat zuweisen, was etwas Speicherplatz verbraucht.

⒈ Vergleich zwischen vom Entwickler definierten Klassen und in PHP intern definierten Klassen

  Die sogenannten vom Entwickler definierten Klassen sind Klassen, die mit der PHP-Sprache definiert sind, während sich intern von PHP definierte Klassen auf die im PHP-Quellcode definierten Klassen beziehen oder eine Klasse, die in einer PHP-Erweiterung definiert ist. Der wesentlichste Unterschied zwischen den beiden ist der Lebenszyklus:

  • Nehmen Sie PHP-FPM als Beispiel. Wenn eine Anfrage eingeht, analysiert PHP die vom Entwickler definierte Klasse und weist ihr den entsprechenden Speicherplatz zu. Später, während der Verarbeitung der Anfrage, ruft PHP diese Klassen entsprechend auf. Nach der Verarbeitung der Anfrage werden diese Klassen schließlich zerstört und der ihnen zugewiesene Speicherplatz freigegeben.

Um Speicherplatz zu sparen, definieren Sie einige Klassen nicht, die nicht tatsächlich im Code verwendet werden. Sie können Autoload verwenden, um diese Klassen abzuschirmen, die nicht tatsächlich verwendet werden, da Autoload eine Klasse nur dann lädt und analysiert, wenn sie verwendet wird. Dies verzögert jedoch den Analyse- und Ladevorgang der Klasse von der Kompilierungsphase des Codes bis zur Ausführung die Code-Phase, die sich auf die Leistung auswirkt

Darüber hinaus ist zu beachten, dass die vom Entwickler angepasste Klasse auch dann analysiert und geladen wird, wenn die Anfrage eintrifft, und wenn die OPCache-Erweiterung aktiviert ist Anfrage. OPCache verbessert nur diese beiden Die Geschwindigkeit jeder Stufe

  • Die in PHP definierten Klassen sind unterschiedlich. Nehmen wir immer noch PHP-FPM als Beispiel: Wenn ein PHP-FPM-Prozess gestartet wird, weist PHP diesen Klassen gleichzeitig dauerhaft Speicherplatz zu, bis der PHP-FPM-Prozess stirbt (um Speicherlecks zu vermeiden, wird PHP-FPM zerstört und dann Neustart nach einer Reihe von Anfragen)
if (EG(full_tables_cleanup)) {
	zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function_full TSRMLS_CC);
	zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class_full TSRMLS_CC);
} else {
	zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function TSRMLS_CC);
	zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class TSRMLS_CC);
}

static int clean_non_persistent_class(zend_class_entry **ce TSRMLS_DC)
{
	return ((*ce)->type == ZEND_INTERNAL_CLASS) ? ZEND_HASH_APPLY_STOP : ZEND_HASH_APPLY_REMOVE;
}

  Wie aus dem obigen Code ersichtlich ist, wird die in PHP definierte Klasse nicht zerstört, wenn die Anfrage endet. Da die in PHP-Erweiterungen definierten Klassen auch zur Kategorie der in PHP definierten Klassen gehören, sollten Sie im Hinblick auf die Einsparung von Speicherplatz einige Erweiterungen, die Sie nicht verwenden, nicht öffnen. Denn sobald die Erweiterung aktiviert ist, werden die in der Erweiterung definierten Klassen analysiert und geladen, wenn der PHP-FPM-Prozess startet.

Der Einfachheit halber passen wir Ausnahmen oft an, indem wir Exception erben. Da die zend_class_entry-Struktur jedoch sehr groß ist, verbraucht sie viel Speicher und verbessert gleichzeitig den Komfort. Klassenbindung. Klassenbindung bezieht sich auf den Vorbereitungsprozess von Klassendaten. Für PHP-interne Definitionsklassen ist der Bindungsprozess der Bindungsprozess ist mit der Kursanmeldung abgeschlossen. Dieser Prozess findet vor der Ausführung des PHP-Skripts statt und findet nur einmal während der Lebensdauer des gesamten PHP-FPM-Prozesses statt.

  Für Klassen, die weder die übergeordnete Klasse erben noch eine Schnittstelle implementieren oder Merkmale verwenden, erfolgt der Bindungsprozess während der Bearbeitungsphase des PHP-Codes und verbraucht nicht zu viele Ressourcen. Diese Art der Klassenbindung erfordert normalerweise nur die Registrierung der Klasse in class_table und die Prüfung, ob die Klasse abstrakte Methoden enthält, aber nicht als abstrakter Typ deklariert ist.

void zend_do_early_binding(TSRMLS_D) /* {{{ */
{
	zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-1];
	HashTable *table;

	while (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)->opcodes) {
		opline--;
	}

	switch (opline->opcode) {
		case ZEND_DECLARE_FUNCTION:
			if (do_bind_function(CG(active_op_array), opline, CG(function_table), 1) == FAILURE) {
				return;
			}
			table = CG(function_table);
			break;
		case ZEND_DECLARE_CLASS:
			if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1 TSRMLS_CC) == NULL) {
				return;
			}
			table = CG(class_table);
			break;
		case ZEND_DECLARE_INHERITED_CLASS:
			{
				/*... ...*/
			}
		case ZEND_VERIFY_ABSTRACT_CLASS:
		case ZEND_ADD_INTERFACE:
		case ZEND_ADD_TRAIT:
		case ZEND_BIND_TRAITS:
			/* We currently don't early-bind classes that implement interfaces */
			/* Classes with traits are handled exactly the same, no early-bind here */
			return;
		default:
			zend_error(E_COMPILE_ERROR, "Invalid binding type");
			return;
	}

/*... ...*/
}

void zend_verify_abstract_class(zend_class_entry *ce TSRMLS_DC)
{
	zend_abstract_info ai;

	if ((ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) && !(ce->ce_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) {
		memset(&ai, 0, sizeof(ai));

		zend_hash_apply_with_argument(&ce->function_table, (apply_func_arg_t) zend_verify_abstract_class_function, &ai TSRMLS_CC);

		if (ai.cnt) {
			zend_error(E_ERROR, "Class %s contains %d abstract method%s and must therefore be declared abstract or implement the remaining methods (" MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT ")",
				ce->name, ai.cnt,
				ai.cnt > 1 ? "s" : "",
				DISPLAY_ABSTRACT_FN(0),
				DISPLAY_ABSTRACT_FN(1),
				DISPLAY_ABSTRACT_FN(2)
				);
		}
	}
}
  Der Bindungsprozess für eine Klasse, die eine Schnittstelle implementiert, ist sehr kompliziert. Der allgemeine Prozess ist wie folgt:

Überprüfen Sie, ob die Schnittstelle implementiert wurde.

Überprüfen Sie, ob die Schnittstelle tatsächlich eine Klasse und nicht die Schnittstelle selbst ist ( Klasse, Schnittstelle, Merkmal) Die zugrunde liegenden Datenstrukturen sind alle zend_class_entry)

Konstanten kopieren und auf mögliche Konflikte prüfen

Methoden kopieren und auf mögliche Konflikte prüfen

    Schnittstelle zu zend_class_entry hinzufügen
  • Es ist zu beachten, dass das sogenannte Kopieren nur die Referenzanzahl von Konstanten, Eigenschaften und Methoden um 1 erhöht. zum Kopieren von Methoden: do_inherit_method Zusätzlich zur Erhöhung des Referenzzählers der entsprechenden Methode wird auch der Referenzzähler der in der Methode definierten statischen Variablen erhöht.
  • ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface TSRMLS_DC)
    {
    	/* ... ... */
    	
    	} else {
    		if (ce->num_interfaces >= current_iface_num) { /* resize the vector if needed */
    			if (ce->type == ZEND_INTERNAL_CLASS) {
    				/*对于内部定义的 class,使用 realloc 分配内存,所分配的内存在进程的生命周期中永久有效*/
    				ce->interfaces = (zend_class_entry **) realloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num));
    			} else {
    				/*对于开发者定义的 class,使用 erealloc 分配内存,所分配的内存只在请求的生命周期中有效*/
    				ce->interfaces = (zend_class_entry **) erealloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num));
    			}
    		}
    		ce->interfaces[ce->num_interfaces++] = iface; /* Add the interface to the class */
    
    		/* Copy every constants from the interface constants table to the current class constants table */
    		zend_hash_merge_ex(&ce->constants_table, &iface->constants_table, (copy_ctor_func_t) zval_add_ref, sizeof(zval *), (merge_checker_func_t) do_inherit_constant_check, iface);
    		/* Copy every methods from the interface methods table to the current class methods table */
    		zend_hash_merge_ex(&ce->function_table, &iface->function_table, (copy_ctor_func_t) do_inherit_method, sizeof(zend_function), (merge_checker_func_t) do_inherit_method_check, ce);
    
    		do_implement_interface(ce, iface TSRMLS_CC);
    		zend_do_inherit_interfaces(ce, iface TSRMLS_CC);
    	}
    }
  •   Die Bindung von Klassen, die Schnittstellen implementieren, verbraucht aufgrund der Notwendigkeit mehrerer Schleifendurchläufe und -prüfungen normalerweise viele CPU-Ressourcen, spart aber Speicherplatz.
  • **interfacesIn diesem Stadium verschiebt PHP die Bindung der Schnittstelle an die Codeausführungsphase, da es davon ausgeht, dass diese Vorgänge bei jeder Anfrage ausgeführt werden
  •   对于 class 继承的绑定,过程与 interface 的绑定类似,但更为复杂。另外有一个值得注意的地方,如果 class 在绑定时已经解析到了父类,则绑定发生在代码编译阶段;否则发生在代码执行阶段。

    // A 在 B 之前申明,B 的绑定发生在编译阶段
    class A { }
    class B extends A { }
    
    // A 在 B 之后申明,绑定 B 时编译器无法知道 A 情况,此时 B 的绑定只能延后到代码执行时
    class B extends A { }
    class A { }
    
    // 这种情况会报错:Class B doesn't exist
    // 在代码执行阶段绑定 C,需要解析 B,但此时 B 有继承了 A,而 A 此时还是未知状态
    class C extends B { }
    class B extends A { }
    class A { }

    如果使用 autoload,并且采用一个 class 对应一个文件的模式,则所有 class 的绑定都只会发生在代码执行阶段

    二、PHP 5 中的 object

    ⒈ object 中的方法

      方法与函数的底层数据结构均为 zend_function。PHP 编译器在编译时将方法编译并添加到 zend_class_entry 的 function_table 属性中。所以,在 PHP 代码运行时,方法已经编译完成,PHP 要做的只是通过指针找到方法并执行。

    typedef union _zend_function {
    	zend_uchar type;
    
    	struct {
    		zend_uchar type;
    		const char *function_name;
    		zend_class_entry *scope;
    		zend_uint fn_flags;
    		union _zend_function *prototype;
    		zend_uint num_args;
    		zend_uint required_num_args;
    		zend_arg_info *arg_info;
    	} common;
    
    	zend_op_array op_array;
    	zend_internal_function internal_function;
    } zend_function;

      当 object 尝试调用方法时,首先会在其对应的 class 的 function_table 中查找该方法,同时还会检查方法的访问控制。如果方法不存在或方法的访问控制不符合要求,object 会尝试调用莫属方法 __call

    static inline union _zend_function *zend_get_user_call_function(zend_class_entry *ce, const char *method_name, int method_len) 
    {
    	zend_internal_function *call_user_call = emalloc(sizeof(zend_internal_function));
    	call_user_call->type = ZEND_INTERNAL_FUNCTION;
    	call_user_call->module = (ce->type == ZEND_INTERNAL_CLASS) ? ce->info.internal.module : NULL;
    	call_user_call->handler = zend_std_call_user_call;
    	call_user_call->arg_info = NULL;
    	call_user_call->num_args = 0;
    	call_user_call->scope = ce;
    	call_user_call->fn_flags = ZEND_ACC_CALL_VIA_HANDLER;
    	call_user_call->function_name = estrndup(method_name, method_len);
    
    	return (union _zend_function *)call_user_call;
    }
    
    static union _zend_function *zend_std_get_method(zval **object_ptr, char *method_name, int method_len, const zend_literal *key TSRMLS_DC)
    {
    	zend_function *fbc;
    	zval *object = *object_ptr;
    	zend_object *zobj = Z_OBJ_P(object);
    	ulong hash_value;
    	char *lc_method_name;
    	ALLOCA_FLAG(use_heap)
    
    	if (EXPECTED(key != NULL)) {
    		lc_method_name = Z_STRVAL(key->constant);
    		hash_value = key->hash_value;
    	} else {
    		lc_method_name = do_alloca(method_len+1, use_heap);
    		/* Create a zend_copy_str_tolower(dest, src, src_length); */
    		zend_str_tolower_copy(lc_method_name, method_name, method_len);
    		hash_value = zend_hash_func(lc_method_name, method_len+1);
    	}
    
    	if (UNEXPECTED(zend_hash_quick_find(&zobj->ce->function_table, lc_method_name, method_len+1, hash_value, (void **)&fbc) == FAILURE)) {
    		if (UNEXPECTED(!key)) {
    			free_alloca(lc_method_name, use_heap);
    		}
    		if (zobj->ce->__call) {
    			return zend_get_user_call_function(zobj->ce, method_name, method_len);
    		} else {
    			return NULL;
    		}
    	}
    
    	/* Check access level */
    	if (fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) {
    		zend_function *updated_fbc;
    
    		/* Ensure that if we're calling a private function, we're allowed to do so.
    		* If we're not and __call() handler exists, invoke it, otherwise error out.
    		*/
    		updated_fbc = zend_check_private_int(fbc, Z_OBJ_HANDLER_P(object, get_class_entry)(object TSRMLS_CC), lc_method_name, method_len, hash_value TSRMLS_CC);
    		if (EXPECTED(updated_fbc != NULL)) {
    			fbc = updated_fbc;
    		} else {
    			if (zobj->ce->__call) {
    				fbc = zend_get_user_call_function(zobj->ce, method_name, method_len);
    			} else {
    				zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : "");
    			}
    		}
    	} else {
    		/* Ensure that we haven't overridden a private function and end up calling
    		* the overriding public function...
    		*/
    		if (EG(scope) &&
    		    is_derived_class(fbc->common.scope, EG(scope)) &&
    		    fbc->op_array.fn_flags & ZEND_ACC_CHANGED) {
    			zend_function *priv_fbc;
    
    			if (zend_hash_quick_find(&EG(scope)->function_table, lc_method_name, method_len+1, hash_value, (void **) &priv_fbc)==SUCCESS
    				&& priv_fbc->common.fn_flags & ZEND_ACC_PRIVATE
    				&& priv_fbc->common.scope == EG(scope)) {
    				fbc = priv_fbc;
    			}
    		}
    		if ((fbc->common.fn_flags & ZEND_ACC_PROTECTED)) {
    			/* Ensure that if we're calling a protected function, we're allowed to do so.
    			* If we're not and __call() handler exists, invoke it, otherwise error out.
    			*/
    			if (UNEXPECTED(!zend_check_protected(zend_get_function_root_class(fbc), EG(scope)))) {
    				if (zobj->ce->__call) {
    					fbc = zend_get_user_call_function(zobj->ce, method_name, method_len);
    				} else {
    					zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : "");
    				}
    			}
    		}
    	}
    
    	if (UNEXPECTED(!key)) {
    		free_alloca(lc_method_name, use_heap);
    	}
    	return fbc;
    }

      这里需要指出的是:

    • 由于 PHP 对大小写不敏感,所以所有的方法名称都会被转为小写(zend_str_tolower_copy())
    • 为了避免不必要的资源消耗,PHP 5.4 开始引入了 zend_literal 结构体,即参数 key
    typedef struct _zend_literal {
    	zval       constant;
    	zend_ulong hash_value;
    	zend_uint  cache_slot;
    } zend_literal;

      其中,constant 记录了转为小写后的字符串,hash_value 则是预先计算好的 hash。这样就避免了 object 每次调用方法都要将方法名称转为小写并计算 hash 值。

    class Foo { public function BAR() { } }
    $a = new Foo;
    $b = 'bar';
    
    $a->bar(); /* good */
    $a->$b(); /* bad */

      在上例中,在代码编译阶段,方法 BAR 被转换成 bar 并添加到 zend_class_entry 的 function_table 中。当发生方法调用时:

    • 第一种情形,在代码编译阶段,方法名称 bar 确定为字符串常量,编译器可以预先计算好其对应的 zend_literal 结构,即 key 参数。这样,代码在执行时相对会更快。
    • 第二种情形,由于在编译阶段编译器对 $b 一无所知,这就需要在代码执行阶段现将方法名称转为小写,然后计算 hash 值。

    ⒉ object 中的属性

      当对一个 class 进行实例化时,object 中的属性只是对 class 中属性的引用。这样,object 的创建操作就会相对轻量化,并且会节省一部分内存空间。

    Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

      如果要对 object 中的属性进行修改,zend 引擎会单独创建一个 zval 结构,只对当前 object 的当前属性产生影响。

    Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

      class 的实例化对应的会在底层创建一个 zend_obejct 数据结构,新创建的 object 会注册到 zend_objects_store 中。zend_objects_store 是一个全局的 object 注册表,同一个对象在该注册表中只能注册一次。

    typedef struct _zend_object {
    	zend_class_entry *ce;
    	HashTable *properties;
    	zval **properties_table;
    	HashTable *guards; /* protects from __get/__set ... recursion */
    } zend_object;
    
    typedef struct _zend_objects_store {/*本质上是一个动态 object_bucket 数组*/
    	zend_object_store_bucket *object_buckets;
    	zend_uint top; /*下一个可用的 handle,handle 取值从 1 开始。对应的在 *object_buckets 中的 index 为 handle - 1*/
    	zend_uint size; /*当前分配的 *object_buckets 的最大长度*/
    	int free_list_head; /*当 *object_bucket 中的 bucket 被销毁后,该 bucket 在 *object_buckets 中的 index 会被有序加入 free_list 链表。free_list_head 即为该链表中的第一个值*/
    } zend_objects_store;
    
    typedef struct _zend_object_store_bucket {
    	zend_bool destructor_called;
    	zend_bool valid; /*值为 1 表示当前 bucket 被使用,此时 store_bucket 中的 store_object 被使用;值为 0 表示当前 bucket 并没有存储有效的 object,此时 store_bucket 中的 free_list 被使用*/
    	zend_uchar apply_count;
    	union _store_bucket {
    		struct _store_object {
    			void *object;
    			zend_objects_store_dtor_t dtor;
    			zend_objects_free_object_storage_t free_storage;
    			zend_objects_store_clone_t clone;
    			const zend_object_handlers *handlers;
    			zend_uint refcount;
    			gc_root_buffer *buffered;
    		} obj;
    		struct {
    			int next; /*第一个未被使用的 bucket 的 index 永远存储在 zend_object_store 的 free_list_head 中,所以 next 只需要记录当前 bucket 之后第一个未被使用的 bucket 的 index*/
    		} free_list;
    	} bucket;
    } zend_object_store_bucket;
    
    ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC)
    {
    	zend_object_value retval;
    
    	*object = emalloc(sizeof(zend_object));
    	(*object)->ce = class_type;
    	(*object)->properties = NULL;
    	(*object)->properties_table = NULL;
    	(*object)->guards = NULL;
    	retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_storage, NULL TSRMLS_CC);
    	retval.handlers = &std_object_handlers;
    	return retval;
    }

       将 object 注册到 zend_objects_store 中以后,将会为 object 创建属性(对相应 class 属性的引用)

    ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type) 
    {
    	int i;
    
    	if (class_type->default_properties_count) {
    		object->properties_table = emalloc(sizeof(zval*) * class_type->default_properties_count);
    		for (i = 0; i < class_type->default_properties_count; i++) {
    			object->properties_table[i] = class_type->default_properties_table[i];
    			if (class_type->default_properties_table[i]) {
    #if ZTS
    				ALLOC_ZVAL( object->properties_table[i]);
    				MAKE_COPY_ZVAL(&class_type->default_properties_table[i], object->properties_table[i]);
    #else
    				Z_ADDREF_P(object->properties_table[i]);
    #endif
    			}
    		}
    		object->properties = NULL;
    	}
    }

      需要指出的是,在创建属性时,如果是非线程安全模式的 PHP,仅仅是增加相应属性的引用计数;但如果是线程安全模式的 PHP,则需要对属性进行深度复制,将 class 的属性全部复制到 object 中的 properties_table 中。

    这也说明,线程安全的 PHP 比非线程安全的 PHP 运行慢,并且更耗费内存

    每个属性在底层都对应一个 zend_property_info 结构:

    typedef struct _zend_property_info {
        zend_uint flags;
        const char *name;
        int name_length;
        ulong h;
        int offset;
        const char *doc_comment;
        int doc_comment_len;
        zend_class_entry *ce;
    } zend_property_info;

      class 中声明的每个属性,在 zend_class_entry 中的 properties_table 中都有一个zend_property_info 与之相对应。properties_table 可以帮助我们快速确定一个 object 所访问的属性是否存在:

    • 如果属性不存在,并且我们尝试向 object 写入该属性:如果 class 定义了 __set 方法,则使用 __set 方法写入该属性;否则会向 object 添加一个动态属性。但无论以何种方式写入该属性,写入的属性都将添加到 object 的 properties_table 中。
    • 如果属性存在,则需要检查相应的访问控制;对于 protected 和 private 类型,则需要检查当前的作用域。

    在创建完 object 之后,只要我们不向 object 中写入新的属性或更新 object 对应的 class 中的属性的值,则 object 所占用的内存空间不会发生变化。

    属性的存储/访问方式:
    zend_class_entry->properties_info 中存储的是一个个的 zend_property_info。而属性的值实际以 zval 指针数组的方式存储在 zend_class_entry->default_properties_table 中。object 中动态添加的属性只会以 property_name => property_value 的形式存储在 zend_object->properties_table 中。而在创建 object 时,zend_class_entry->properties_table 中的值会被逐个传递给 zend_object->properties_table。
    zend_literal->cache_slot 中存储的 int 值为 run_time_cache 中的索引 index。run_time_cache 为数组结构,index 对应的 value 为访问该属性的 object 对应的 zend_class_entry;index + 1 对应的 value 为该属性对应的 zend_property_info 。在访问属性时,如果 zend_literal->cache_slot 中的值不为空,则可以通过 zend_literal->cache_slot 快速检索得到 zend_property_info 结构;如果为空,则在检索到 zend_property_info 的信息之后会初始化 zend_literal->cache_slot。

    属性名称的存储方式
    private 属性:"\0class_name\0property_name"
    protected 属性:"\0*\0property_name"
    public 属性:"property_name"

       执行以下代码,看看输出结果

    class A {
        private $a = &#39;a&#39;;
        protected $b = &#39;b&#39;;
        public $c = &#39;c&#39;;
    }
    
    class B extends A {
        private $a = &#39;aa&#39;;
        protected $b = &#39;bb&#39;;
        public $c = &#39;cc&#39;;
    }
    
    class C extends B {
        private $a = &#39;aaa&#39;;
        protected $b = &#39;bbb&#39;;
        public $c = &#39;ccc&#39;;
    }
    
    var_dump(new C());

    zend_object 中 guards 的作用
    guards 的作用是对 object 的重载提供递归保护。

    class Foo {
        public function __set($name, $value) {
            $this->$name = $value;
        }
    }
    
    $foo = new Foo;
    $foo->bar = &#39;baz&#39;;
    var_dump($foo->bar);

       以上代码中,当为 foo动态设置foo 动态设置bar 属性时会调用 __set 方法。但 $bar 属性在 Foo 中并不存在,按照常理,此时又会递归调用 __set 方法。为了避免这种递归调用,PHP 会使用 zend_guard 来判断当前是否已经处于重载方法的上下文中。

    typedef struct _zend_guard {
        zend_bool in_get;
        zend_bool in_set;
        zend_bool in_unset;
        zend_bool in_isset;
        zend_bool dummy; /* sizeof(zend_guard) must not be equal to sizeof(void*) */
    } zend_guard;

    ⒊ object 的引用传递

      首先需要申明:object 并不是引用传递。之所以会出现 object 是引用传递的假象,原因在于我们传递给函数的参数中所存储的只是 object 在 zend_objects_store 中的 ID(handle)。通过这个 ID,我们可以在 zend_objects_store 中查找并加载真正的 object,然后访问并修改 object 中的属性。

    PHP 中,函数内外是两个不同的作用域,对于同一变量,在函数内部对其修改不会影响到函数外部。但通过 object 的 ID(handle)访问并修改 object 的属性并不受此限制。

    $a = 1;
    
    function test($a) {
        $a = 3;
        echo $a; // 输出 3
    }
    
    test($a);
    
    echo $a; // 输出 1

    Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

    同一个 object 在 zend_objects_store 中只存储一次。要向 zend_objects_store 中写入新的对象,只能通过 new 关键字、unserialize 函数、反射、clone 四种方式。

    ⒋ $this

      $this 在使用时会自动接管当前对象,PHP 禁止对 this进行赋值操作。任何对this 进行赋值操作。任何对this 的赋值操作都会引起错误

    static zend_bool opline_is_fetch_this(const zend_op *opline TSRMLS_DC)
    {
    	if ((opline->opcode == ZEND_FETCH_W) && (opline->op1_type == IS_CONST)
    	    && (Z_TYPE(CONSTANT(opline->op1.constant)) == IS_STRING)
    	    && ((opline->extended_value & ZEND_FETCH_STATIC_MEMBER) != ZEND_FETCH_STATIC_MEMBER)
    	    && (Z_HASH_P(&CONSTANT(opline->op1.constant)) == THIS_HASHVAL)
    	    && (Z_STRLEN(CONSTANT(opline->op1.constant)) == (sizeof("this")-1))
    	    && !memcmp(Z_STRVAL(CONSTANT(opline->op1.constant)), "this", sizeof("this"))) {
    	    return 1;
    	} else {
    	    return 0;
    	}
    }
    
    /* ... ... */
    if (opline_is_fetch_this(last_op TSRMLS_CC)) {
    	zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");
    }
    /* ... ... */

       在 PHP 中进行方法调用时,对应执行的 OPCode 为 INIT_METHOD_CALL。以 $a->foo() 为例,在 INIT_METHOD_CALL 中,Zend 引擎知道是由 $a 发起的方法调用,所以 Zend 引擎会把 $a 的值存入全局空间。在实际执行方法调用时,对应执行的 OPCode 为 DO_FCALL。在 DO_FCALL 中,Zend 引擎会将之前存入全局空间的 $a 赋值给 $this 的指针,即 EG(This):

    if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) {
        should_change_scope = 1;
        EX(current_this) = EG(This);
        EX(current_scope) = EG(scope);
        EX(current_called_scope) = EG(called_scope);
        EG(This) = EX(object); /* fetch the object prepared in previous INIT_METHOD opcode and affect it to EG(This) */
        EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope : NULL;
        EG(called_scope) = EX(call)->called_scope;
    }

       在实际执行方法体中的代码时,如果出现使用 $this 进行方法调用或属性赋值的情况,如 $this->a = 8 对应的将执行 OPCode ZEND_ASSIGN_OBJ,此时将从 EG(This) 取得 $this 的值

    static zend_always_inline zval **_get_obj_zval_ptr_ptr_unused(TSRMLS_D)
    {
    	if (EXPECTED(EG(This) != NULL)) {
    		return &EG(This);
    	} else {
    		zend_error_noreturn(E_ERROR, "Using $this when not in object context");
    		return NULL;
    	}
    }

      Zend 引擎在构建方法堆栈时,$this 会被存入符号表,就像其他的变量一样。这样,当使用 $this 进行方法调用或将 $this 作为方法的参数时,Zend 引擎将从符号表中获取 $this

    if (op_array->this_var != -1 && EG(This)) {
        Z_ADDREF_P(EG(This)); /* For $this pointer */
        if (!EG(active_symbol_table)) {
            EX_CV(op_array->this_var) = (zval **) EX_CV_NUM(execute_data, op_array->last_var + op_array->this_var);
            *EX_CV(op_array->this_var) = EG(This);
        } else {
            if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void **) EX_CV_NUM(execute_data, op_array->this_var))==FAILURE) {
                Z_DELREF_P(EG(This));
            }
        }
    }

       最后是关于作用域的问题,当进行方法调用时,Zend 引擎会将作用域设置为 EG(scope)。EG(scope) 是 zend_class_entry 类型,也就是说,在方法中任何关于 object 的操作的作用域都是 object 对应的 class。对属性的访问控制的检查也是同样:

    ZEND_API int zend_check_protected(zend_class_entry *ce, zend_class_entry *scope) 
    {
    	zend_class_entry *fbc_scope = ce;
    
    	/* Is the context that&#39;s calling the function, the same as one of
    	* the function&#39;s parents?
    	*/
    	while (fbc_scope) {
    		if (fbc_scope==scope) {
    			return 1;
    		}
    		fbc_scope = fbc_scope->parent;
    	}
    
    	/* Is the function&#39;s scope the same as our current object context,
    	* or any of the parents of our context?
    	*/
    	while (scope) {
    		if (scope==ce) {
    			return 1;
    		}
    		scope = scope->parent;
    	}
    	return 0;
    }
    
    static zend_always_inline int zend_verify_property_access(zend_property_info *property_info, zend_class_entry *ce TSRMLS_DC)
    {
    	switch (property_info->flags & ZEND_ACC_PPP_MASK) {
    		case ZEND_ACC_PUBLIC:
    			return 1;
    		case ZEND_ACC_PROTECTED:
    			return zend_check_protected(property_info->ce, EG(scope));
    		case ZEND_ACC_PRIVATE:
    			if ((ce==EG(scope) || property_info->ce == EG(scope)) && EG(scope)) {
    				return 1;
    			} else {
    				return 0;
    			}
    			break;
    	}
    	return 0;
    }

      正是由于上述特性,所以以下代码可以正常运行

    class A
    {
    	private $a;
    
    	public function foo(A $obj)
    	{
    		$this->a = &#39;foo&#39;;
    		$obj->a  = &#39;bar&#39;; /* yes, this is possible */
    	}
    }
    
    $a = new A;
    $b = new A;
    $a->foo($b);

    PHP 中 object 的作用域是 object 对应的 class

    ⒌ 析构方法 destruct

      在 PHP 中,不要依赖 destruct 方法销毁 object。因为当 PHP 发生致命错误时,destruct 方法并不会被调用。

    ZEND_API void zend_hash_reverse_apply(HashTable *ht, apply_func_t apply_func TSRMLS_DC)
    {
    	Bucket *p, *q;
    
    	IS_CONSISTENT(ht);
    
    	HASH_PROTECT_RECURSION(ht);
    	p = ht->pListTail;
    	while (p != NULL) {
    		int result = apply_func(p->pData TSRMLS_CC);
    
    		q = p;
    		p = p->pListLast;
    		if (result & ZEND_HASH_APPLY_REMOVE) {
    			zend_hash_apply_deleter(ht, q);
    		}
    		if (result & ZEND_HASH_APPLY_STOP) {
    			break;
    		}
    	}
    	HASH_UNPROTECT_RECURSION(ht);
    }
    
    static int zval_call_destructor(zval **zv TSRMLS_DC) 
    {
    	if (Z_TYPE_PP(zv) == IS_OBJECT && Z_REFCOUNT_PP(zv) == 1) {
    		return ZEND_HASH_APPLY_REMOVE;
    	} else {
    		return ZEND_HASH_APPLY_KEEP;
    	}
    }
    
    void shutdown_destructors(TSRMLS_D) 
    {
    	zend_try {
    		int symbols;
    		do {
    			symbols = zend_hash_num_elements(&EG(symbol_table));
    			zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC);
    		} while (symbols != zend_hash_num_elements(&EG(symbol_table)));
    		zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
    	} zend_catch {
    		/* if we couldn&#39;t destruct cleanly, mark all objects as destructed anyway */
    		zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
    	} zend_end_try();
    }

      在调用 destruct 方法时,首先会从后往前遍历整个符号表,调用所有引用计数为 1 的 object 的 destruct 方法;然后从前往后遍历全局 object store,调用每个 object 的 destruct 方法。在此过程中如果有任何错误发生,就会停止调用 destruct 方法,然后将所有 object 的 destruct 方法都标记为已调用过的状态。

    class Foo { public function __destruct() { var_dump("destroyed Foo"); } }
    class Bar { public function __destruct() { var_dump("destroyed Bar"); } }
    
    // 示例 1
    $a = new Foo;
    $b = new Bar;
    "destroyed Bar"
    "destroyed Foo"
    
    // 示例 2
    $a = new Bar;
    $b = new Foo;
    "destroyed Foo"
    "destroyed Bar"
    
    // 示例 3
    $a = new Bar;
    $b = new Foo;
    $c = $b; /* $b 引用计数加 1 */
    "destroyed Bar"
    "destroyed Foo"
    
    // 示例 4
    class Foo { public function __destruct() { var_dump("destroyed Foo"); die();} } /* notice the die() here */
    class Bar { public function __destruct() { var_dump("destroyed Bar"); } }
    
    $a = new Foo;
    $a2 = $a;
    $b = new Bar;
    $b2 = $b;
    
    "destroyed Foo"

       另外,不要在 destruct 方法中添加任何重要的代码

    class Foo
    {
    	public function __destruct() { new Foo; } /* PHP 最终将崩溃 */
    }

    PHP 中对象的销毁分为两个阶段:首先调用 destruct 方法(zend_object_store_bucket->bucket->obj->zend_objects_store_dtor_t),然后再释放内存(zend_object_store_bucket->bucket->obj->zend_objects_free_object_storage_t)。

    之所以分为两个阶段执行是因为 destruct 中执行的是用户级的代码,即 PHP 代码;而释放内存的代码在系统底层运行。释放内存会破坏 PHP 的运行环境,为了使 destruct 中的 PHP 代码能正常运行,所以分为两个阶段,这样,保证在释放内存阶段 object 已经不被使用。

    三、PHP 7 中的 object

      与 PHP 5 相比,PHP 7 中的 object 在用户层并没有基本没有什么变化;但在底层实现上,在内存和性能方面做了一些优化。

    ⒈ 在内存布局和管理上的优化

       ① 首先,在 zval 中移除了之前的 zend_object_value 结构,直接嵌入了 zend_object。这样,既节省了内存空间,同时提高了通过 zval 查找 zend_object 的效率

    /*PHP 7 中的 zend_object*/
    struct _zend_object {
        zend_refcounted   gc;
        uint32_t          handle;
        zend_class_entry *ce;
        const zend_object_handlers *handlers;
        HashTable        *properties;
        zval              properties_table[1];
    };
    
    /*PHP 5 中的 zend_object_value*/
    typedef struct _zend_object_value {
        zend_object_handle handle;
        const zend_object_handlers *handlers;
    } zend_object_value;

       在 PHP 5 中通过 zval 访问 object,先要通过 zva 中的 zend_object_value 找到 handle,然后通过handle 在 zend_object_store 中找到 zend_object_store_bucket,然后从 bucket 中解析出 object。在 PHP 7 中,zval 中直接存储了 zend_object 的地址指针。

       ② 其次,properties_table 利用了 struct hack 特性,这样使得 zend_object 和 properties_table 存储在一块连续的内存空间。同时,properties_table 中直接存储了属性的 zval 结构。

       ③ guards 不再出现在 zend_object 中。如果 class 中定义了魔术方法( __set__get__isset__unset ),则 guards 存储在 properties_table 的第一个 slot 中;否则不存储 guards。

       ④ zend_object_store 及 zend_object_store_bucket 被移除,取而代之的是一个存储各个 zend_object 指针的 C 数组,handle 为数组的索引。此外,之前 bucket 中存储的 handlers 现在移入 zend_object 中;而之前 bucket 中的 dtor、free_storege、clone 现在则移入了 zend_object_handlers。

    struct _zend_object_handlers {
        /* offset of real object header (usually zero) */
        int                                     offset;
        /* general object functions */
        zend_object_free_obj_t                  free_obj;
        zend_object_dtor_obj_t                  dtor_obj;
        zend_object_clone_obj_t                 clone_obj;
        /* individual object functions */
        // ... 其他与 PHP 5 相同
    };

    ⒉  底层自定义 object 的变化(PHP 扩展中会用到自定义 object)

    /*PHP 5 中的 custom_object*/
    struct custom_object {
        zend_object std;
        my_custom_type *my_buffer;
        // ...
    };
    
    /*PHP 7 中的 custom_object*/
    struct custom_object {
        my_custom_type *my_buffer;
        // ...
        zend_object std;
    };

       由于 PHP 7 的 zend_object 中使用了 struct hack 特性来保证 zend_object 内存的连续,所以自定义 object 中的 zend_object 只能放在最后。而 zval 中存储的只能是 zend_object,为了能通过 zend_object 顺利解析出 custom_object ,在 zend_object 的 handlers 中记录了 offset。

    Ein kurzer Vergleich der Objekte in PHP 7 und PHP 5

    推荐学习:《PHP视频教程

Das obige ist der detaillierte Inhalt vonEin kurzer Vergleich der Objekte in PHP 7 und PHP 5. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen