header-object

Bạn chẳng biết chút gì về biến trong PHP

 

Bài viết này ở cấp độ tương đối khó, mình viết hướng tới đối tượng là các bạn đã có kinh nghiệm lập trình PHP. Vì vậy, một số kiến thức mang tính cơ sở mình sẽ không giải thích chi tiết.

Phần demo thực hiện với PHP-5.3.10 trên hệ điều hành Windows, thanh ghi kích thước 32 bit.

 

Phần nổi của tảng băng

Phần này chúng ta sẽ trao đổi với nhau về những điểm cơ bản về biến mà khi các bạn học lập trình PHP vẫn thường hay được dạy. Một phần trong số nội dung này các bạn có thể đọc ở tài liệu PHP Manual (Chapter7 + Chapter8).

 

Thắc mắc số 1: Một biến trong ngôn ngữ lập trình thông thường gồm có bao nhiêu thành phần, là những thành phần nào? Trong PHP thì sao?

Trong các ngôn ngữ lập trình thông dụng, một biến sẽ gồm có 6 thành phần cấu thành bao gồm:

  1. Tên biến
  2. Kiểu dữ liệu của biến
  3. Giá trị
  4. Địa chỉ
  5. Phạm vi
  6. Thời gian sống

Bạn phải phân biệt giữa phạm vi và thời gian sống của biến. Thời gian sống của biến cho ta biết một biến có thể nào bị hủy một cách bất thường (bởi lập trình viên hoặc hệ thống) hay không.

Đối với PHP, nếu chỉ xét trong số 6 thành phần trên thì nó có 5 thành phần và thiếu 1 là địa chỉ. Thiếu ở đây có nghĩa rằng bạn không thể nào biết được địa chỉ của 1 biến ở trong chương trình PHP.

Đây là 1 câu hỏi dễ mà chắc chắn phần lớn các bạn sẽ đều trả lời được. Chúng ta tiếp tục với thắc mắc sau.

 

Thắc mắc số 2: Thế ngoài 5 thành phần đã nói ở trên thì một biến trong PHP còn có thành phần nào khác?

Câu trả lời là có, một biến bất kì trong PHP sẽ có thêm 2 thành phần khác nữa cũng rất quan trọng. Đoạn code sau sẽ cho chúng ta thấy (Với điều kiện Web-Server của bạn phải có XDebug).

<?php
$a='danweb.vn';
xdebug_debug_zval('a');

Đoạn mã trên đơn giản là gán chuỗi ‘danweb.vn’ cho biến có tên $a, phương thức ở dòng ngay sau đó cho chúng ta kết quả như sau:

bien1

Theo như bạn quan sát trên, mỗi biến trong PHP còn có thêm 2 thành phần có tên là refcountis_ref. Chúng đều liên quan đến tham chiếu giữa các biến trong PHP, Tham chiếu giữa các biến trong PHP là 1 chủ đề cũng rất hay mà nếu có dịp mình sẽ chia sẻ với các bạn trong một bài viết khác.

 

Thắc mắc số 3: Trong PHP, có tất cả bao nhiêu kiểu dữ liệu cho các biến?

Trong PHP gồm 8 kiểu dữ liệu cơ bản (còn 1 số kiểu không cơ bản khác) và chia ra làm 3 nhóm:

1. Nhóm kiểu dữ liệu vô hướng (scalar type) gồm 4 loại:

  • boolean
  • integer
  • float (floating-point number, aka 'double')
  • string

2. Nhóm kiểu dữ liệu phức hợp (compound type) gồm 2 kiểu:

  • array
  • object

3. Nhóm kiểu dữ liệu đặc biệt (special type) gồm 2 kiểu:

  • resource
  • NULL

 

Thắc mắc số 4: Theo bạn, mỗi khi dùng 1 biến kiểu integer thì trình thông dịch PHP sẽ cấp phát cho biến đó bao nhiêu bộ nhớ? Và cũng liên quan đến thắc mắc này: làm thế nào mà một biến trong PHP có thể lưu trữ các kiểu bất kì?

Chắc chắn là nhiều bạn sẽ vui vẻ và đáp ngay rằng một biến integer sẽ chiếm hết 4 bytes bộ nhớ. Có chắc đơn giản vậy không, chúng ta sẽ cùng đi thử nghiệm để biết chính xác.

Mở chương trình viết mã PHP yêu thích của bạn và nhập đoạn mã đơn giản như sau:

<?php
$a= memory_get_usage();
$b = 0;
echo memory_get_usage()-$a;

Hàm memory_get_usage() trả về cho chúng ta giá trị là một số nguyên, giá trị này là số bytes bộ nhớ đã cấp phát để thực thi đoạn script cho tới thời điểm gọi hàm này.

Chạy đoạn script PHP này trên trình duyệt, bạn sẽ thấy kết quả xuất ra (ở dòng số 4) là 160. Các bạn lưu ý là giữa 2 thời điểm gọi memory_get_usage() chúng ta đã sử dụng cả thảy 2 biến kiểu số nguyên (integer) là $a và $b.

Để cho chắc trước khi kết luận, chúng ta thử lại lần thứ 2 bằng cách thêm vô 1 biến  ngay sau dòng thứ 2 như sau:

<?php
$a= memory_get_usage();
$b = 0;
$c = 1000;
echo memory_get_usage()-$a;

Kết quả thu được là số 240. Ở đây, như bạn thấy chúng ta đã sử dụng 3 biến integer gồm $a, $b và $c.

Từ 2 điều trên, chúng ta có thể tạm kết luận rằng mỗi biến kiểu integer sẽ chiếm tất cả 80 bytes bộ nhớ. Chắc chắn đọc tới đây bạn sẽ mắt chữ O miệng chữ A, “cái quái gì xảy ra vậy?” vì chúng ta đã được học rằng biến kiểu integer sẽ chiếm 4 bytes bộ nhớ có thể lưu trữ các giá trị trong khoảng từ -(216 – 1) tới +216. 4 bytes và 80 bytes là 2 con số khác quá xa nhau.

Vâng, với một biến integer chúng ta sẽ tốn tối thiểu (tại sao là tối thiểu chúng ta sẽ đề cập sau) là 80 bytes bộ nhớ. Một con số lớn hơn mong đợi rất nhiều, và lý do vì sao lại như thế chính là phần chìm của tảng băng.

 

Phần chìm của tảng băng

Ở phần này, mình sẽ trình bày cùng các bạn lý do tại sao một biến integer (hoặc float, boolean) lại chiếm những 80 bytes bộ nhớ. Qua đó bạn cũng sẽ hiểu được tại sao 1 biến trong PHP có thể lưu trữ kiểu bất kì. Để hiểu được những điều chúng ta sẽ trao đổi ngay sau đây bạn cần có một số kiến thức về ngôn ngữ lập trình C.

 

Vấn đề thứ nhất: cấu trúc lưu trữ zval (zval container)

PHP lưu trữ các biến sử dụng cấu trúc zval (tất nhiên cấu trúc này được cài đặt bằng C trong mã nguồn của module PHP). Bạn có thể chứng thực điều này tại đây: http://php.net/manual/en/features.gc.refcounting-basics.php

Ta hãy cùng nhau xem, cấu trúc này được cài đặt bằng C như sau:

  struct _zval_struct {
    zvalue_value value;     // giá trị của 1 biến sẽ có kiểu zvalue_value
    zend_uint refcount__gc; // số references (tham chiếu) tới vùng nhớ này
    zend_uchar type;        // kiểu của biến
    zend_uchar is_ref__gc;  // biến này có phải chỉ là references không (&)
};
  

Có nghĩa rằng các biến trong PHP nếu nhìn ở khía cạnh bản chất (khi được cài đặt trong C) thì đều có kiểu _zval_struct (là một struct) như ở trong đoạn code trên (_zval_struct viết tắt là zval).

Dừng ở đây và đối chiếu với thắc mắc số 2 ở bên trên. Bây giờ thì bạn đã nhìn thấy phần cài đặt cho các thành phần is_ref và refcount của một biến ở trong C tương ứng chính là is_ref__gc và refcount__gc trong cấu trúc zval (gc là viết tắt của cụm từ Garbage Collection)

Kích cỡ (độ lớn vùng nhớ được cấp phát bởi trình biên dịch) của một struct zval là bao nhiêu? với C hay C++ thì kích cỡ của struct bằng tổng kích thước của các thành phần tạo nên nó. zend_unit có kích thước 4 bytes, zend_uchar có kích thước 1 bytes. Còn zvalue_value là một union được cài đặt như sau:

typedef union _zvalue_value {
	long lval;                // lưu giá trị integers hoặc booleans
	double dval;              // lưu giá trị floats (doubles)
	struct {                  // Lưu giá trị strings
		char *val;            //     con trỏ đầu chuỗi
		int len;              //     độ dài chuỗi
	} str;
	Hashtable *ht;            // kiểu dữ liệu arrays (hash tables)
	zend_object_value obj;    // kiểu dữ liệu objects
} zvalue_value;

Nếu đã từng lập trình C hoặc C++, các bạn chắc biết một điều rằng kích cỡ (được cấp phát vùng nhớ) của một biến union sẽ bằng kích cỡ của thành phần lớn nhất trong nó. Trong số 5 thành phần của union trên thì zvalue_value-->str có kích thước lớn nhất. Nó gồm một con trỏ kiểu char (8 bytes) và một biến thành phần kiểu int (4 bytes) tổng cộng là 12 bytes.

Tuy nhiên, 12 bytes không phải là con số đẹp vì việc lưu trữ trong bộ nhớ sẽ theo từng block 8 bytes. Bởi vậy thực tế trình biên dịch C sẽ cấp phát 16 bytes bộ nhớ cho biến kiểu zvalue_value.

Vậy, tính tổng kích thước các thành phần trong struct _zval_struct lại là: 4 + 1 + 1 + 16 = 22 bytes. Tương tự như trên, 22 không chia hết cho 8 vì vậy thực tế trình biên dịch sẽ cấp phát cả thảy 24 bytes cho một biến nào đó có kiểu _zval_struct.

 
Vấn đề thứ 2: Cấu trúc cho phép chương trình có thể tự động “thu lượm vùng nhớ rác” - Garbage Collection.

Hi vọng là tới đây các bạn vẫn chưa ngán và sẽ theo dõi tiếp tục các nội dung khá “lằng nhằng” sau.

Trong các ngôn ngữ không cho phép can thiệp tới con trỏ vùng nhớ như PHP hay Java thì việc “thu lượm vùng nhớ rác ” một cách tự động (hay cách gọi khác là quản lý thu gom vùng nhớ động) là rất quan trọng. Thuật ngữ chuyên ngành là Garbage Collection.

PHP 5.3 sử dụng cơ chế thu lượm như được giới thiệu một cách tóm lược (được trình bày khá dân dã) ở đây: http://php.net/manual/en/features.gc.collecting-cycles.php. Bản chất cơ chế này được xây dựng dựa trên một nghiên cứu được xuất bản năm 2001 của 2 tác giả David F. Bacon và V.T. Rajan. Nếu bạn quan tâm có thể tham khảo paper này tại đây: http://www.research.ibm.com/people/d/dfb/papers/Bacon01Concurrent.pdf.

Mình sẽ làm đơn giản hóa vấn đề để các bạn tiện theo dõi. Để thực hiện được việc tự động quản lý vùng nhớ động, thực tế người ta tạo ra một struct (tất nhiên là trong mã C tạo nên  PHP) và mỗi cấu trúc _zvalue_value ở trên đóng vai trò là 1 thành phần như trong code sau:

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;

Nhìn vào cấu trúc trên chúng ta có thể tính được ngay: thành phần z có kích thước 24 bytes (vừa tính ở trên). union u có kích thước 8 bytes (kích thước của 1 con trỏ trong C). Như vậy mỗi struct _zval_gc_info sẽ được cấp phát 24+8 = 32 bytes cả thảy.

 

Vấn đề thứ 3: Việc quản lý cấp phát vùng nhớ động (Memory Manager)

Như bạn đã biết Zend xây dựng lên PHP, cơ chế cấp phát và quản lý vùng nhớ động của PHP được gọi là The Zend Memory Manager, nếu quan tâm các bạn có thể tham khảo tại đây: http://php.net/manual/en/internals2.memory.php

Tiếp tục, chúng ta hãy làm cho vấn đề được đơn giản. Đi kèm với mỗi biến PHP phải tạo ra thêm một thành phần có tên là _zend_mm_block được cài đặt như sau:

typedef struct _zend_mm_block {
    zend_mm_block_info info;
#if ZEND_DEBUG
    unsigned int magic;
# ifdef ZTS
    THREAD_T thread_id;
# endif
    zend_mm_debug_info debug;
#elif ZEND_MM_HEAP_PROTECTION
    zend_mm_debug_info debug;
#endif
} zend_mm_block;

Với đoạn code C ở trên, trong trường hợp thông thường (không bật Zend Debug, không synchronized các Thread). Chúng ta chỉ cần cấp phát bộ nhớ cho thành phần info của struct, thành phần này lại có kiểu zend_mm_block_info. Cấu trúc của zend_mm_block_info được cài đặt như sau:

typedef struct _zend_mm_block_info {
#if ZEND_MM_COOKIES
    size_t _cookie;
#endif
    size_t _size; // kích thước được allocation
    size_t _prev; // khối liền trước
} zend_mm_block_info;

Vì không dùng Zend Debug nên tất nhiên thành phần _cookie trong cấu trúc _zend_mm_block_info ở trên không dùng đến. Như thế chúng ta chỉ tốn vùng nhớ cấp phát cho 2 thành phần còn lại là _size và _prev. Chúng đều có kiểu size_t có kích thước 8 bytes. Như vậy, với mỗi biến chúng ta sẽ phải sử dụng thêm một struct zend_mm_block có kích thước (tối thiểu) là 8+8 = 16 bytes.

Gom thêm với phần đã tính ở trên, vậy là tới thời điểm này cứ mỗi biến được tạo ra là tiêu tốn của hệ thống hết: 32 + 16 = 48 bytes.

 

Vấn đề thứ 4: Tên của biến trong PHP và một vài điều “lạ kì”

Chúng ta sẽ cùng nhau bàn về 2 (trong số khá nhiều) yếu tố có thể coi là “lạ kì” của tên biến trong PHP mà mình lựa chọn để giới thiệu ở đây với các bạn:

Xem đoạn code PHP thứ nhất:

<?php
$a = 'b';
$b = 100;
echo $$a;

Đoạn code này khi thực thi cho chúng ta kết quả là số 100 (giá trị của $b) ở trên trang. Ở đây mình đang muốn tập trung sự chú ý của các bạn tới việc tên của biến $b lại có thể y như một giá trị (của biến $a). Nói cách khác ta có thể hiểu $$a = $($a) = $(b). Ok, tên biến chẳng khác gì giá trị của biến.

Đoạn code PHP thứ 2 sẽ gây 1 chút sửng sốt:

<?php
$a = new stdClass();
$b = "Dân Web";
$a->$b = 'danweb.vn';
var_dump($a);

Kết quả thu được như sau

bien2

Sửng sốt ở chỗ nào? Khi học về biến trong PHP chắc hẳn ông thầy nào cũng dạy các bạn rằng “tên biến trong PHP bắt đầu bằng đô-la, không khoảng trắng, không kí tự đặc biệt bla bla…” và kể cả trong PHP Manual (Chapter8) cũng nói vậy. Uầy, tất cả chỉ là “mị dân”.

Vì như ở trên, các bạn thấy đối tượng $a có một thuộc tính có tên là “Dân Web”, rõ ràng đây là một chuỗi unicode chứa khoảng trắng đàng hoàng.

Lấy 2 ví dụ nhỏ trên để mình hướng các bạn tới một kết luận rằng, trong PHP bản thân tên biến cũng được lưu trữ giống như giá trị của một biến (khác). Và tất nhiên nó chỉ không có phần Memory Manager, vì vậy riêng tên của biến đã chiếm hết cả thảy 32 bytes bộ nhớ (y như đã phân tích trong vấn đề 1 và vấn đề 2 ở trên).

Đây là trường hợp tên biến tiết kiệm nhất ($ kèm với 3 kí tự ANSI – vì mỗi kí tự là 1 bytes). Nếu bạn đặt dài hơn thì tất nhiên phần bộ nhớ mà biến (có tên dài) đó chiếm dụng sẽ tăng lên.

Vậy, tới đây với mỗi biến chúng ta mất tối thiểu 32 bytes cho tên + 48 bytes cho các thành phần còn lại = 80 bytes tròn chĩnh.

 

Một vài kết luận

80 bytes bộ nhớ cho một biến integer trong PHP, một con số thật quá xa so với 4 bytes trong C hoặc C++ đúng không các bạn. Vì vậy hãy dùng biến một cách tiết kiệm và thông minh.

Và nếu tên của 1 biến mà dài ra thì vùng bộ nhớ phải cấp phát sẽ tăng thêm đáng kể. Bởi vậy, nếu được cũng không nên đặt tên biến quá dài sẽ làm cho bộ nhớ chương trình cần dùng đến tăng lên chóng mặt.

Như bạn thấy, chỉ một vấn đề nhỏ là biến (nhỏ đến nỗi phần lớn lập trình viên chả bao giờ quan tâm tới nó) mà đã có rất nhiều thứ có thể bàn cùng nhau. Nếu bạn còn điều gì chưa hiểu hoặc muốn trao đổi thêm về vấn đề này thì hãy đưa ý kiến trên group của danweb.vn ở địa chỉ: http://facebook.com/groups/danweb/ nhé.

Để kết, mình xin chia sẻ một câu nói của Bill Gate: “A great lathe operator commands several times the wage of an average lathe operator, but a great writer of software code is worth 10,000 times the price of an average software writer”. Đúng như vậy, nếu thực sự yêu thích việc lập trình bạn hãy từng bước nâng cao kiến thức bằng việc tăng thời gian tư duy, giảm thời gian viết mã để trở thành một lập trình viên thông minh (great writer of software).

Đào Ngọc Giang

Bình luận  

 
0 #5 Hùng Itechco Thứ 6-08-16 08:54
bài viết hay
Trích dẫn
 
 
0 #4 fgh Thứ 2-08-16 07:32
fghffghf
Trích dẫn
 
 
+1 #3 Sand Thứ 5-10-13 19:15
Great. Hay quá ạ, các bài viết của anh này tuyệt quá. Có ai biết blog của tác giả này không chỉ em với ới ới :-?

Mà sao em không thấy có các bài viết mới vậy ban quản trị?
Trích dẫn
 
 
+1 #2 khangpv80 Thứ 6-08-13 11:02
Great. Phải là người nắm rât vững kiến thức cơ bản cề C, và cũng đã làm qua PHP hoặc language tương tự mới có thể trình bày được như vậy. Thậm chí dev đã đi làm có kinh nghiệm cũng chưa nắm được.

Mình thấy cái bài viết discuss về "bạn chỉ là end-user khi sử dụng PHP, .NET, Java..." cuả Huỳnh Côn Đức, nên tham khao bài này để nội dung bớt sáo rỗng và thực tế hơn.
Trích dẫn
 
 
+1 #1 matico Thứ 4-07-13 10:53
bài viết tuyệt quá
Trích dẫn
 

Thêm ý kiến


Security code
Làm mới


2

Facebook

Thống kê truy cập

Hiện có 814 khách đang truy cập
2491975