Giảm Bộ Nhớ Sử Dụng Trong Python

Mở đầu

Bắt đầu bằng một class đơn giản như sau:

1class DataItem(object):
2    def __init__(self, name, age, address):
3        self.name = name
4        self.age = age
5        self.address = address

Bạn nghĩ một đối tượng của class trên sẽ chiếm bao nhiêu bộ nhớ. Chúng ta cùng tiến hành một vài thí nghiệm nho nhỏ bên dưới.

1dx = DataItem("Alex Black", 42, "-")
2print ("sys.getsizeof(dx):", sys.getsizeof(dx))
3>> sys.getsizeof(dx): 56

Kết quả ra là 56 bytes, khá hợp lý phải không các bạn. Thử với một ví dụ khác xem sao nhỉ.

1dy = DataItem("Alex Black", 42, "I am working at MWG")
2print ("sys.getsizeof(dy):", sys.getsizeof(dy))
3>> sys.getsizeof(dy): 56

Kết quả vẫn là 56 bytes. Có cái gì đó sai sai ở đây không nhỉ?

Chúng ta thực nghiệm một vài thí nghiệm khác để chứng thực.

 1print (sys.getsizeof(""))
 2>> 49
 3print (sys.getsizeof("1"))
 4>> 50
 5print (sys.getsizeof(1))
 6>> 28
 7print (sys.getsizeof(dict()))
 8>> 240
 9print (sys.getsizeof({}))
10>> 240
11print (sys.getsizeof(list()))
12>> 64
13print (sys.getsizeof([]))
14>> 64
15print (sys.getsizeof(()))
16>> 48

Một điều cực kỳ bất ngờ đã xuất hiện ở đây. Một chuỗi rỗng chiếm đến tận 49 bytes, một dictionary rỗng, không chứa phần tử nào chiếm đến 240 bytes, và một list rỗng chiếm tới 64 bytes. Rõ ràng, python đã lưu một số thứ gì đó ngoài dữ liệu của mình.

Đi sâu vào thử tìm hiểu những thứ ’linh kiện’ linh tinh mà python đã kèm theo cho chúng ta là gì nhé.

Đầu tiên, chúng ta sẽ cần một hàm in ra những thứ mà python đã ’nhúng’ thêm vào class DataItem chúng ta khai báo ở trên.

1def dump(obj):
2  for attr in dir(obj):
3    print("  obj.%s = %r" % (attr, getattr(obj, attr)))

và dump biến dy ra thôi

 1dump(dy)
 2
 3obj.__class__ = <class '__main__.DataItem'>
 4  obj.__delattr__ = <method-wrapper '__delattr__' of DataItem object at 0x000001A64A6DD0F0>
 5  obj.__dict__ = {'name': 'Alex Black', 'age': 42, 'address': 'i am working at MWG'}
 6  obj.__dir__ = <built-in method __dir__ of DataItem object at 0x000001A64A6DD0F0>
 7  obj.__doc__ = None
 8  obj.__eq__ = <method-wrapper '__eq__' of DataItem object at 0x000001A64A6DD0F0>
 9  obj.__format__ = <built-in method __format__ of DataItem object at 0x000001A64A6DD0F0>
10  obj.__ge__ = <method-wrapper '__ge__' of DataItem object at 0x000001A64A6DD0F0>
11  obj.__getattribute__ = <method-wrapper '__getattribute__' of DataItem object at 0x000001A64A6DD0F0>
12  obj.__gt__ = <method-wrapper '__gt__' of DataItem object at 0x000001A64A6DD0F0>
13  obj.__hash__ = <method-wrapper '__hash__' of DataItem object at 0x000001A64A6DD0F0>
14  obj.__init__ = <bound method DataItem.__init__ of <__main__.DataItem object at 0x000001A64A6DD0F0>>
15  obj.__init_subclass__ = <built-in method __init_subclass__ of type object at 0x000001A64A5DE738>
16  obj.__le__ = <method-wrapper '__le__' of DataItem object at 0x000001A64A6DD0F0>
17  obj.__lt__ = <method-wrapper '__lt__' of DataItem object at 0x000001A64A6DD0F0>
18  obj.__module__ = '__main__'
19  obj.__ne__ = <method-wrapper '__ne__' of DataItem object at 0x000001A64A6DD0F0>
20  obj.__new__ = <built-in method __new__ of type object at 0x000000005C2DC580>
21  obj.__reduce__ = <built-in method __reduce__ of DataItem object at 0x000001A64A6DD0F0>
22  obj.__reduce_ex__ = <built-in method __reduce_ex__ of DataItem object at 0x000001A64A6DD0F0>
23  obj.__repr__ = <method-wrapper '__repr__' of DataItem object at 0x000001A64A6DD0F0>
24  obj.__setattr__ = <method-wrapper '__setattr__' of DataItem object at 0x000001A64A6DD0F0>
25  obj.__sizeof__ = <built-in method __sizeof__ of DataItem object at 0x000001A64A6DD0F0>
26  obj.__str__ = <method-wrapper '__str__' of DataItem object at 0x000001A64A6DD0F0>
27  obj.__subclasshook__ = <built-in method __subclasshook__ of type object at 0x000001A64A5DE738>
28  obj.__weakref__ = None
29  obj.address = 'i am working at MWG'
30  obj.age = 42
31  obj.name = 'Alex Black'

Wow, có vẻ khá là đồ sộ nhỉ.

Trên github, có một hàm có sẵn tính toán số lượng bộ nhớ mà object chiếm được dựa vào cách truy xuất trực tiếp từng trường dữ liệu của đối tượng và tính toán kích thước

 1import sys
 2
 3def get_size(obj, seen=None):
 4    """Recursively finds size of objects"""
 5    size = sys.getsizeof(obj)
 6    if seen is None:
 7        seen = set()
 8    obj_id = id(obj)
 9    if obj_id in seen:
10        return 0
11    # Important mark as seen *before* entering recursion to gracefully handle
12    # self-referential objects
13    seen.add(obj_id)
14    if isinstance(obj, dict):
15        size += sum([get_size(v, seen) for v in obj.values()])
16        size += sum([get_size(k, seen) for k in obj.keys()])
17    elif hasattr(obj, '__dict__'):
18        size += get_size(obj.__dict__, seen)
19    elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
20        size += sum([get_size(i, seen) for i in obj])
21    return size

thử với 2 biến dx và dy của chúng ta xem sao

1>>> print ("get_size(d1):", get_size(dx))
2get_size(d1): 466
3>>> print ("get_size(d1):", get_size(dy))
4get_size(d1): 484

Chúng tốn lần lượt là 466 và 484 bytes. Có vẻ đúng đó nhỉ.

Điều chúng ta quan tâm lúc này là có cách nào để giảm bộ nhớ tiêu thụ của một object hay không?

Giảm bộ nhớ tiêu thụ của một đối tượng trong python

Tất nhiên là sẽ có cách giảm. Python là một ngôn ngữ thông dịch, và nó cho phép chúng ta mở rộng lớp bất kể lúc nào bằng cách thêm một/ nhiều trường dữ liệu.

1dz = DataItem("Alex Black", 42, "-")
2dz.height = 1.80
3print ( get_size(dz))
4>> 484

Chính vì lý do này, trình biên dịch sẽ tốn thêm một đống bộ nhớ tạm để chúng ta có thể dễ dàng mở rộng một lớp trong tương lai. Nếu chúng ta “ép buộc” trình biên dịch, nói rằng chúng ta chỉ có nhiêu đó trường, và bỏ phần dư thừa đi.

1class DataItem(object):
2    __slots__ = ['name', 'age', 'address']
3    def __init__(self, name, age, address):
4        self.name = name
5        self.age = age
6        self.address = address

Và thử lại

1
2dz = DataItem("Alex Black", 42, "i am working at MWG")
3print ("sys.getsizeof(dz):", get_size(dz))
4
5>>sys.getsizeof(dz): 64

Các bạn thấy gì không, bộ nhớ tiêu thụ chỉ là “64 bytes”. Dung lượng đã giảm đi hơn “7 lần” so với model class ban đầu. Tuy nhiên, chúng ta sẽ không thể mở rộng class dễ dàng như xưa nữa.

1>>> dz.height = 1.80
2Traceback (most recent call last):
3  File "<stdin>", line 1, in <module>
4AttributeError: 'DataItem' object has no attribute 'height'

Thử tạo một đối tượng có 1000 phần tử và kiểm tra thử.

 1class DataItem(object):
 2    __slots__ = ['name', 'age', 'address']
 3    def __init__(self, name, age, address):
 4        self.name = name
 5        self.age = age
 6        self.address = address
 7
 8
 9data = []
10
11tracemalloc.start()
12start =datetime.datetime.now()
13for p in range(100000):
14    data.append(DataItem("Alex", 42, "middle of nowhere"))
15
16end =datetime.datetime.now()
17snapshot = tracemalloc.take_snapshot()
18top_stats = snapshot.statistics('lineno')
19total = sum(stat.size for stat in top_stats)
20print("Total allocated size: %.1f MB" % (total / (1024*1024)))
21print("Total execute time:",(end-start).microseconds)
22
23>> Total allocated size: 6.9 MB
24>> Total execute time: 232565

Bỏ dòng slots = [’name’, ‘age’, ‘address’] đi thử

 1
 2class DataItem(object):
 3    def __init__(self, name, age, address):
 4        self.name = name
 5        self.age = age
 6        self.address = address
 7
 8
 9data = []
10
11tracemalloc.start()
12start =datetime.datetime.now()
13for p in range(100000):
14    data.append(DataItem("Alex", 42, "middle of nowhere"))
15end =datetime.datetime.now()
16snapshot = tracemalloc.take_snapshot()
17top_stats = snapshot.statistics('lineno')
18total = sum(stat.size for stat in top_stats)
19print("Total allocated size: %.1f MB" % (total / (1024*1024)))
20print("Total execute time:",(end-start).microseconds)
21
22>> Total allocated size: 16.8 MB
23>> Total execute time: 240772

So sánh thử, chúng ta thấy rằng số lượng RAM giảm đi khá nhiều, thời gian thực thi khá tương đương nhau (có giảm một chút).

Cảm ơn các bạn đã theo dõi. Hẹn gặp bạn ở các bài viết tiếp theo.

Comments