Tổng hợp một số ý tưởng về optimize performance trong Python, được tìm hiểu và lấy từ nhiều nguồn khác nhau.
Python và High Performance thường có "myth" là không đi đôi với nhau, đương nhiên điều này còn phụ thuộc vào việc lựa chọn distribution, vì mỗi phiên bản sẽ có những approach khác nhau để giúp Python chạy nhanh và tốt hơn (Ví dụ: PyPy với JIT compiler). Do đó, trong phạm vi bài viết này, mình sẽ chỉ đề cập đến distribution chính thức đó là CPython với version mới nhất tính đến thời điểm hiện tại (18/02/2024) là 3.12.
Function reference
Vấn đề
Hãy xem đoạn code dưới đây:
a = []
print(id(a.append))
print(id(a.append))
Kết quả chúng ta nhận về sẽ là gì? Ở đây mình đã dùng hàm built-in id của Python để lấy thông tin "unique identifier" của append
của class list
. Nếu chúng ta chạy đoạn code trên, chúng ta sẽ thấy với mỗi lệnh print nó sẽ trả về id khác nhau của hàm append. Chúng ta có thể chạy bao nhiêu lần tuỳ thích, và mỗi lần dòng print sẽ in ra kết quả khác nhau.
Không chỉ trong Python, nếu bạn sử dụng Java hay C# thì điều tương tự cũng sẽ xảy ra.
Giải thích
Mình sẽ tập trung vào ngôn ngữ Python, các ngôn ngữ khác các bạn có thể tìm hiểu thêm trên các diễn đàn, mặc dù mỗi ngôn ngữ có thể có những cách tối ưu khác nhau. Tuy nhiên, về mặt ý tưởng sẽ có nhiều nét tương đồng.
Hãy bắt đầu bằng một ví dụ:
class Test:
def rand_method(self):
return "Hello world"
a = Test()
# This is what we usually do
print(a.rand_method()) # print "Hello world"
# Yes, this is not illegal
print(Test.rand_method(a)) # Also print "Hello world"
# Can we just use it like this?
print(Test.rand_method()) # Nope, this will be complained: TypeError: Test.rand_method() missing 1 required positional argument: 'self'
Tại sao chúng ta có thể gọi trực tiếp member function (non-static) của class Test
trực tiếp thông qua class Test
?
Theo document của Python 3:
When an instance method object is created by retrieving a user-defined function object from a class via one of its instances, its
__self__
attribute is the instance, and the method object is said to be bound. The new method’s__func__
attribute is the original function object.
When an instance method object is called, the underlying function (
__func__
) is called, inserting the class instance (__self__
) in front of the argument list. For instance, when C is a class which contains a definition for a function f(), and x is an instance of C, calling x.f(1) is equivalent to calling C.f(x, 1).
Và document cũng lý giải tại sao trong ví dụ ban đầu mỗi lần lấy id
của method append
nó lại trả về kết quả khác nhau.
Note that the transformation from function object to instance method object happens each time the attribute is retrieved from the instance. In some cases, a fruitful optimization is to assign the attribute to a local variable and call that local variable.
Về mặt design, các member functions của 1 class sẽ thuộc về chính class
đó, và mỗi khi chúng ta gọi một member function thông qua interpreter sẽ:
- Định danh
class
mà instance đó thuộc về - Tìm kiếm tên hàm đó ở trong
class
, resolve thành một variable mới trongcall stack
. - Gọi hàm đó và truyền (pass by reference) đối số
self
- instance, và những arguments khác.
a = Test()
a.rand_method()
# interpreter will translate to
A.rand_method(a)
Reference thêm một câu trả lời trên Stack Overflow liên quan đến C# cho bạn nào quan tâm: https://stackoverflow.com/a/1298133
For loop và List comprehensions:
List Comprehensions nhanh hơn For Loop?
Khi mình mới sử dụng Python để lập trình các ứng dụng, mình thấy khá nhiều anh chị nói mình nên dùng list comprehensions
thay vì dùng for loop nếu có thể. Lúc đó mình chỉ đơn thuần nghĩ đó là một cách viết khác, ngắn gọn và dễ đọc hơn của for loop, và sẽ không có ảnh hưởng gì nếu chúng ta tiếp tục dùng For Loop bình thường. Tuy nhiên, nếu tìm hiểu kỹ, ta sẽ thấy có một số "cost" khi dùng For Loop, hãy xem ví dụ sau:
def test1():
a = [i for i in range(10000)]
return a
def test2():
a = []
for i in range(10000):
a.append(i)
return a
if __name__ == '__main__':
# Tested on Macbook Pro M1, 16GB RAM
import timeit
print(timeit.timeit("test1()", setup="from __main__ import test1")) # 147.92s
print(timeit.timeit("test2()", setup="from __main__ import test2")) # 185.81s
Với ví dụ trên, chúng ta chỉ đang đơn thuần để tạo list
. Một số điểm chúng ta có thể để ý như sau:
- Do member method của instance được built với mỗi lần gọi, do đó trong
test2
với mỗi vòng loop, chúng ta đang phải load lại hàmappend
1 lần (Bạn có thể xem ByteCode với moduledis
để thấy điều này rõ hơn vớiLOAD_ATTR
được gọi. Trong khi vớitest1
,LIST_APPEND
sẽ được gọi trực tiếp). - Khi kết thúc vòng lặp cuối cùng,
test2
vẫn sẽ truy cập được biếni
trong khitest1
sẽ raise exception nếu chúng ta cố gắng truy cậpi
.
Tuy nhiên, theo những gì mình thử kiểm tra, nếu loại bỏ đi ý một bằng các tạo 1 local variable để chứa reference đến append = a.append
thì không hẳn nó sẽ giúp for loop chạy nhanh hơn.
Một số kết luận
Trong Python chúng ta có khá nhiều công cụ được tạo sẵn để làm việc với các kiểu dữ liệu khác nhau. Nếu bạn đang làm việc cần phải lặp qua một iterator
thì bạn có thể tham khảo một số cách ở đây để chọn lựa hàm phù hợp với mình: Looping Techniques - Python 3 Documentation.
Theo quan điểm của mình, list comprehensions
là một cách rất hay để khởi tạo list
(hay iterator nói chung), tuy nhiên nó không phải là kim chỉ nam cho tất cả:
- Khi có nhiều logic phải thực hiện khi khởi tạo
list
, for loop nên được sử dụng để giúp cho code dễ đọc và maintain (mình nghĩ chỉ nên có tối đa 2 if else trong 1 lần sử dụnglist comprehensions
, nếu không nó sẽ dài và tương đối khó hiểu). - Nếu bạn không có ý định khởi tạo các iterators mà chỉ đơn thuần muốn loop qua để cập nhật dữ liệu,
list comprehensions
sẽ không phải là lựa chọn lý tưởng của bạn. - Function reference khi được assign vào biến local, sẽ giúp loại bỏ đi
LOAD_ATTR
mỗi lần for loop, nhưng theo kiểm nghiệm của mình thì việc này thậm chí còn đang làm chậm đi (một chút) khi không dùng local variable, tuy nhiên đây có thể chỉ đơn thuần do máy tính của mình, nếu có nhiều bằng chứng xác thực hơn mình sẽ cập nhật thêm. - Tạo
list
vớilist comprehensions
nhanh hơn tương đối rõ ràng khi số lượng dữ liệu đầu vào lớn.
Mong rằng các bạn đã hiểu thêm về cách methods hoạt động với instance và tại sao self
luôn là keyword đầu tiên của bất cứ member functions nào.