- Đây là kết quả benchmark đo lường một cách có hệ thống các chỉ số hiệu năng về tính toán, bộ nhớ và I/O của Python, định lượng thời gian và mức sử dụng bộ nhớ của từng thao tác
- Về tốc độ, bài viết đưa ra độ trễ tương đối của nhiều thao tác khác nhau như truy cập thuộc tính 14ns, thêm vào danh sách 29ns, mở tệp 9μs, phản hồi FastAPI 8.6μs
- Về bộ nhớ, bài viết nêu các con số cụ thể như chuỗi rỗng 41 byte, số nguyên 28 byte, danh sách rỗng 56 byte, từ điển rỗng 64 byte, tiến trình rỗng 16MB
- Ở các mảng như cấu trúc dữ liệu, tuần tự hóa, xử lý bất đồng bộ, bài viết so sánh chênh lệch hiệu năng giữa thư viện chuẩn và các thư viện thay thế (
orjson, msgspec...)
- Bài học chính được nhấn mạnh gồm: overhead bộ nhớ cao của object Python, tra cứu rất nhanh của dict/set, hiệu quả tiết kiệm bộ nhớ của
__slots__, và cần nhận thức rõ overhead của xử lý bất đồng bộ
Tổng quan
- Tài liệu này tổng hợp các chỉ số hiệu năng mà lập trình viên Python nên biết, với số đo thực tế về tốc độ thao tác và mức sử dụng bộ nhớ
- Benchmark được thực hiện trên môi trường CPython 3.14.2, Mac Mini M4 Pro (ARM, 14 lõi, 24GB RAM)
- Kết quả tập trung vào so sánh tương đối, đồng thời mã nguồn và dữ liệu được công khai trên kho GitHub
Mức sử dụng bộ nhớ (Memory Costs)
- Một tiến trình Python rỗng sử dụng 15.73MB bộ nhớ
- Chuỗi có kích thước cơ bản là 41 byte, cộng thêm 1 byte cho mỗi ký tự
- Ví dụ: chuỗi rỗng 41B, chuỗi 100 ký tự 141B
- Kiểu số: số nguyên nhỏ (0–256) 28B, số nguyên lớn (1000) cũng 28B, số nguyên rất lớn (10ⁱ⁰⁰) 72B, số thực dấu phẩy động 24B
- Kích thước cơ bản của collection: list 56B, dict 64B, set 216B
- Với 1.000 phần tử: list 35.2KB, dict 63.4KB, set 59.6KB
- Instance của class: class thông thường (5 thuộc tính) 694B, class dùng
__slots__ 212B
- Với 1.000 instance: class thông thường 165.2KB, class
__slots__ 79.1KB
Các thao tác cơ bản (Basic Operations)
- Phép toán số học: cộng số nguyên 19ns, cộng số thực 18.4ns, nhân số nguyên 19.4ns
- Thao tác chuỗi: nối chuỗi 39.1ns, f-string 64.9ns,
.format() 103ns, định dạng % 89.8ns
- Thao tác với list:
append() 28.7ns, list comprehension (1.000 phần tử) 9.45μs, vòng lặp for tương đương 11.9μs
- List comprehension nhanh hơn khoảng 26% so với vòng lặp for
Truy cập và lặp qua collection (Collection Access and Iteration)
- Truy cập theo khóa/chỉ số: tra cứu dict 21.9ns, kiểm tra phần tử trong set 19ns, truy cập chỉ số list 17.6ns
- Kiểm tra phần tử trong list (1.000 phần tử) là 3.85μs, chậm hơn khoảng 200 lần so với set/dict
- Kiểm tra độ dài:
len() với list 18.8ns, dict 17.6ns, set 18ns
- Lặp: list (1.000 phần tử) 7.87μs, dict 8.74μs,
sum() 1.87μs
Class và thuộc tính (Class and Object Attributes)
- Tốc độ truy cập thuộc tính: cả class thông thường và class
__slots__ đều đọc ở mức 14.1ns, ghi khoảng 16ns
- Các thao tác khác: đọc
@property 19ns, getattr() 13.8ns, hasattr() 23.8ns
- Khi dùng
__slots__, mức tiết kiệm bộ nhớ lớn hơn 2 lần, còn tốc độ truy cập gần như tương đương
JSON và tuần tự hóa (JSON and Serialization)
- Hiệu năng của thư viện thay thế so với thư viện chuẩn
orjson tuần tự hóa object phức tạp trong 310ns, nhanh hơn hơn 8 lần so với json là 2.65μs
msgspec là 445ns, ujson là 1.64μs
- Ở giải tuần tự hóa,
orjson cũng nhanh nhất với 839ns
- Pydantic:
model_dump_json() 1.54μs, model_validate_json() 2.99μs
Framework web (Web Frameworks)
- Với cùng một phản hồi JSON, FastAPI 8.63μs, Starlette 8.01μs, Litestar 8.19μs, Flask 16.5μs, Django 18.1μs
- Tốc độ phản hồi của FastAPI nhanh hơn khoảng 2 lần so với Django
I/O tệp (File I/O)
- Mở và đóng tệp 9.05μs, đọc 1KB 10μs, đọc 1MB 33.6μs
- Ghi: 1KB 35.1μs, 1MB 207μs
- Pickle nhanh hơn
json khoảng 2 lần ở cả tuần tự hóa lẫn giải tuần tự hóa (pickle.dumps() 1.3μs, json.dumps() 2.72μs)
Cơ sở dữ liệu và cache (Database and Persistence)
- SQLite: insert 192μs, select 3.57μs, update 5.22μs
- diskcache: set 23.9μs, get 4.25μs
- MongoDB: insert 119μs, find_one 121μs
- SQLite nhanh nhất về tốc độ đọc, còn diskcache có hiệu năng ghi tốt
Overhead của gọi hàm và ngoại lệ (Function and Call Overhead)
- Gọi hàm: hàm rỗng 22.4ns, method 23.3ns, lambda 19.7ns
- Xử lý ngoại lệ: try/except (không phát sinh lỗi) 21.5ns, khi phát sinh ngoại lệ là 139ns
- Kiểm tra kiểu:
isinstance() 18.3ns, so sánh type() 21.8ns
Overhead bất đồng bộ (Async Overhead)
- Tạo coroutine 47ns,
run_until_complete 27.6μs
asyncio.sleep(0) 39.4μs, gather(10 coroutines) 55μs
- So với gọi hàm đồng bộ (20ns), thực thi bất đồng bộ (28μs) chậm hơn khoảng 1.000 lần
Bài học chính (Key Takeaways)
- Overhead bộ nhớ của object Python là rất lớn, ngay cả list rỗng cũng dùng 56 byte
- Tra cứu dict và set nhanh hơn hàng trăm lần so với tìm kiếm trong list
- Các thư viện JSON thay thế như
orjson, msgspec nhanh hơn chuẩn từ 3 đến 8 lần
- Xử lý bất đồng bộ có overhead lớn, nên chỉ khuyến nghị dùng khi thật sự cần tính song song
__slots__ có thể giảm bộ nhớ xuống còn một nửa hoặc thấp hơn mà gần như không mất hiệu năng
1 bình luận
Ý kiến trên Hacker News
Nhiều người nói rằng “nếu phải bận tâm tới các con số độ trễ (latency) trong Python thì nên dùng ngôn ngữ khác”, nhưng tôi không đồng ý.
Những codebase quy mô lớn như Instagram, Dropbox, OpenAI cũng đã phát triển bằng Python. Rốt cuộc rồi cũng sẽ gặp vấn đề hiệu năng, và điều quan trọng là có khả năng giải quyết chúng ngay trong Python thay vì phải chuyển sang ngôn ngữ khác.
Phần lớn vấn đề hiệu năng không đến từ giới hạn của ngôn ngữ mà từ mã kém hiệu quả. Ví dụ như các vòng lặp lặp lại lời gọi hàm 10.000 lần một cách không cần thiết.
Bài Python latency quiz tôi làm cũng đáng tham khảo.
Trớ trêu thay, ngay khi những con số này trở nên quan trọng thì Python không còn là công cụ phù hợp cho công việc đó nữa.
Trên thực tế, điều quan trọng là instrument code (bằng công cụ như pyspy) và tìm ra bottleneck. Nếu bạn đang phải lo tốc độ thêm phần tử vào list, thì phép toán đó không nên được thực hiện trong Python.
Chính nhờ khả năng tương tác giữa Python và C mà cách tiếp cận này khả thi. Zig cũng đang ngày càng tốt hơn. Tôi sẽ không điều khiển máy bay bằng Python, nhưng cảm nhận về tài nguyên vẫn rất quan trọng.
Biết một chuỗi rỗng chiếm bao nhiêu byte thực ra không có nhiều ý nghĩa. Điều quan trọng là hiểu độ phức tạp thời gian và không gian.
Quan trọng hơn việc biết int là 28 byte là xác định xem chương trình có đáp ứng yêu cầu hiệu năng hay không, và nếu không thì tìm thuật toán tốt hơn.
Ví dụ, việc nối chuỗi có độ phức tạp O(n²) cũng ảnh hưởng tới thiết kế f-string trong Python.
Dictionary được dùng rộng khắp trong Python vì nó nhanh, cũng là cùng một logic.
Những con số này đóng vai trò biện minh bằng số liệu cho kiến thức ngầm đó.
Điều này gợi tôi nhớ đến bài viết về vấn đề Eric Raymond gặp phải khi dùng Reposurgeon để migrate GCC.
Tiêu đề hơi gây nhầm lẫn, nhưng thực ra đây là bản nhại lại bài viết năm 2012 của Jeff Dean “Latency Numbers Every Programmer Should Know”.
Kiểu chơi chữ trong tiêu đề như vậy khá phổ biến trong các bài báo khoa học CS.
Đây là tài liệu nội bộ phục vụ thiết kế RAM vs Disk cho công cụ tìm kiếm thời kỳ đầu của Google.
Sau này các con số thay đổi do sự xuất hiện của flash memory, và cũng có giai thoại rằng Jeff đã tạo ra thuật toán nén để phục vụ trực tiếp dữ liệu hệ gen từ flash.
Phần lớn lập trình viên Python nên tập trung vào những việc quan trọng hơn các chi tiết hiệu năng mức thấp này.
Những tài liệu như vậy tốt để tham khảo, nhưng trên thực tế hiếm khi cần.
Phần giải thích kích thước chuỗi là sai. Python có ba loại chuỗi dùng 1, 2 hoặc 4 byte cho mỗi ký tự.
Xem chi tiết trong blog này.
Tiêu đề và ví dụ trong bài hơi thiếu chính xác.
Ví dụ, câu “item in set nhanh hơn item in list 200 lần” là nói về kiểm tra membership, chứ không phải so sánh tốc độ iteration.
Dù vậy, nhìn chung hình thức và cấu trúc vẫn khá hấp dẫn.
Bài thiếu đo thời gian tạo class instance.
Sau khi refactor code, tôi đổi một cấu trúc list đơn giản sang class thì thời gian chạy tăng từ vài micro giây lên vài giây.
Giá mà có đo cả trường hợp này.
Có thể vấn đề là do lạm dụng class. Có lúc cấu trúc list đơn giản lại tốt hơn.
Khả năng cao hơn là bạn đã dùng lập trình hướng đối tượng sai cách.
Tốt hơn nên đăng code lên StackOverflow hoặc CodeReview.SE để nhận góp ý.
Tôi thấy bài này thú vị khi đọc dưới góc nhìn “có phải Python hiện đại đang có gì đó sai từ gốc không”.
Nhưng tôi không đồng ý với lập luận rằng ai cũng phải biết tất cả những con số này.
Chỉ cần có trực giác về một vài phép toán cốt lõi là đủ.
Phạm vi small int caching của Python không phải 0~256 mà là -5~256.
Vì vậy người mới học thường hay nhầm lẫn giữa đồng nhất (is) và bằng nhau (==).