Giảm bộ nhớ sử dụng trong python

- Phạm Duy Tùng

Mở đầu

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

class DataItem(object):
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        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.

dx = DataItem("Alex Black", 42, "-")
print ("sys.getsizeof(dx):", sys.getsizeof(dx))
>> 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ỉ.

dy = DataItem("Alex Black", 42, "I am working at MWG")
print ("sys.getsizeof(dy):", sys.getsizeof(dy))
>> 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.

print (sys.getsizeof(""))
>> 49
print (sys.getsizeof("1"))
>> 50
print (sys.getsizeof(1))
>> 28
print (sys.getsizeof(dict()))
>> 240
print (sys.getsizeof({}))
>> 240
print (sys.getsizeof(list()))
>> 64
print (sys.getsizeof([]))
>> 64
print (sys.getsizeof(()))
>> 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.

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

và dump biến dy ra thôi

dump(dy)

obj.__class__ = <class '__main__.DataItem'>
  obj.__delattr__ = <method-wrapper '__delattr__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__dict__ = {'name': 'Alex Black', 'age': 42, 'address': 'i am working at MWG'}
  obj.__dir__ = <built-in method __dir__ of DataItem object at 0x000001A64A6DD0F0>
  obj.__doc__ = None
  obj.__eq__ = <method-wrapper '__eq__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__format__ = <built-in method __format__ of DataItem object at 0x000001A64A6DD0F0>
  obj.__ge__ = <method-wrapper '__ge__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__getattribute__ = <method-wrapper '__getattribute__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__gt__ = <method-wrapper '__gt__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__hash__ = <method-wrapper '__hash__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__init__ = <bound method DataItem.__init__ of <__main__.DataItem object at 0x000001A64A6DD0F0>>
  obj.__init_subclass__ = <built-in method __init_subclass__ of type object at 0x000001A64A5DE738>
  obj.__le__ = <method-wrapper '__le__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__lt__ = <method-wrapper '__lt__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__module__ = '__main__'
  obj.__ne__ = <method-wrapper '__ne__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__new__ = <built-in method __new__ of type object at 0x000000005C2DC580>
  obj.__reduce__ = <built-in method __reduce__ of DataItem object at 0x000001A64A6DD0F0>
  obj.__reduce_ex__ = <built-in method __reduce_ex__ of DataItem object at 0x000001A64A6DD0F0>
  obj.__repr__ = <method-wrapper '__repr__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__setattr__ = <method-wrapper '__setattr__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__sizeof__ = <built-in method __sizeof__ of DataItem object at 0x000001A64A6DD0F0>
  obj.__str__ = <method-wrapper '__str__' of DataItem object at 0x000001A64A6DD0F0>
  obj.__subclasshook__ = <built-in method __subclasshook__ of type object at 0x000001A64A5DE738>
  obj.__weakref__ = None
  obj.address = 'i am working at MWG'
  obj.age = 42
  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

import sys

def get_size(obj, seen=None):
    """Recursively finds size of objects"""
    size = sys.getsizeof(obj)
    if seen is None:
        seen = set()
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    # Important mark as seen *before* entering recursion to gracefully handle
    # self-referential objects
    seen.add(obj_id)
    if isinstance(obj, dict):
        size += sum([get_size(v, seen) for v in obj.values()])
        size += sum([get_size(k, seen) for k in obj.keys()])
    elif hasattr(obj, '__dict__'):
        size += get_size(obj.__dict__, seen)
    elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
        size += sum([get_size(i, seen) for i in obj])
    return size

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

>>> print ("get_size(d1):", get_size(dx))
get_size(d1): 466
>>> print ("get_size(d1):", get_size(dy))
get_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.

dz = DataItem("Alex Black", 42, "-")
dz.height = 1.80
print ( get_size(dz))
>> 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.

class DataItem(object):
    __slots__ = ['name', 'age', 'address']
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address

Và thử lại


dz = DataItem("Alex Black", 42, "i am working at MWG")
print ("sys.getsizeof(dz):", get_size(dz))

>>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.

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

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

class DataItem(object):
    __slots__ = ['name', 'age', 'address']
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address


data = []

tracemalloc.start()
start =datetime.datetime.now()
for p in range(100000):
    data.append(DataItem("Alex", 42, "middle of nowhere"))
    
end =datetime.datetime.now()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
total = sum(stat.size for stat in top_stats)
print("Total allocated size: %.1f MB" % (total / (1024*1024)))
print("Total execute time:",(end-start).microseconds)

>> Total allocated size: 6.9 MB
>> Total execute time: 232565

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


class DataItem(object):
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address


data = []

tracemalloc.start()
start =datetime.datetime.now()
for p in range(100000):
    data.append(DataItem("Alex", 42, "middle of nowhere"))
end =datetime.datetime.now()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
total = sum(stat.size for stat in top_stats)
print("Total allocated size: %.1f MB" % (total / (1024*1024)))
print("Total execute time:",(end-start).microseconds)

>> Total allocated size: 16.8 MB
>> 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.

Nếu bạn thấy nội dung của bài viết thật sự hữu ích và bạn muốn đóng góp cho blog để có thêm nhiều bài viết chất lượng hơn nữa, các bạn có thể ủng hộ blog bằng một cốc trà hoặc một cốc cà phê nhỏ qua https://nhantien.momo.vn/7abl2tSivGa hoặc paypal.me/tungduypham. Sự ủng hộ của các bạn là nguồn động viên quý giá để chúng tôi có thêm động lực và chia sẻ nhiều điều mà chúng tôi tìm hiểu được đến với cộng đồng. Trân trọng cảm ơn.