38 điểm bởi GN⁺ 2026-01-02 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đâ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

 
GN⁺ 2026-01-02
Ý 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.

    • Tôi phụ trách tối ưu hiệu năng cho các hệ thống viết bằng Python. Nhưng các con số này không có ý nghĩa cho tới khi chúng thực sự trở thành vấn đề. Khi có vấn đề thì tôi tự đo trực tiếp. Nếu viết code theo kiểu cố tiết kiệm từng lời gọi method thì sẽ đánh mất các ưu điểm của Python.
    • Ngay cả các phép toán cơ bản Python cũng chậm. Những việc đơn giản như gọi hàm hay truy cập dictionary cũng chậm. Thực ra Python tồn tại được là nhờ các thư viện dựa trên C/C++ (như Numpy).
    • Những con số này không chỉ là vấn đề của Python. Với Zig cũng phải cân nhắc CPU cycle hay cache miss. Ngôn ngữ nào cũng có độ trễ của những phép toán nhất định. Có thể có lý do để không dùng Python, nhưng đây không phải lý do đó.
    • Một số phép toán có thể được cải thiện nếu dùng module thay thế. Biết những điều này là quan trọng, nhưng ai thực sự cần thì có lẽ đã biết rồi. Dù vậy Python vẫn là ngôn ngữ tuyệt vời để prototyping.
    • Hệ thống build của chúng tôi cũng viết bằng Python, nên tôi muốn cải thiện hiệu năng mà vẫn giữ Python. Vì thế các con số này rất quan trọng.
  • 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.

    • Cách tiếp cận thực tế là giữ code Python nhưng đẩy phần cốt lõi về hiệu năng xuống extension C hoặc Rust. numpy, pandas, PyTorch đều làm vậy.
      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.
    • Tôi đã làm việc với Python 20 năm nhưng chưa bao giờ cần phải biết những con số này. Thay vào đó tôi giải quyết bằng profiling và các công cụ như Cython, SWIG, JIT.
    • Nếu ứng dụng quan trọng tới mức các con số này đáng được bận tâm, thì tôi nghĩ Python quá high-level nên khó tối ưu.
    • Nhưng tôi đã xây dựng pipeline dữ liệu quy mô lớn bằng Python. Kết hợp turbodbc + pandas cho tốc độ ở mức C++. Dùng nhiều bộ nhớ hơn, nhưng xét chi phí nhân công thì hiệu quả hơn nhiều.
      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.
    • Những con số này là phương án cuối cùng. Chỉ nên cân nhắc sau khi đã xử lý hết các bottleneck thông thường như disk I/O, mạng, độ phức tạp thuật toán.
  • 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.

    • Nhưng hiệu năng luôn là một abstraction bị rò rỉ. Dù ta có ý thức hay không, nó vẫn ảnh hưởng đến toàn bộ code.
      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 đó.
    • Việc int chiếm 28 byte thực sự quan trọng trong những bài toán phải tạo ra lượng lớn object.
      Đ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.

    • Nếu viết một bài với tiêu đề như “latency numbers considered harmful is all you need” thì chắc sẽ cực kỳ nổi trong giới học thuật.
    • Nhưng có vẻ tác giả bài này viết một cách nghiêm túc. Không phải người đọc hiểu sai tiêu đề.
    • Muốn tiêu đề hợp lý thì các con số phải thực sự hữu ích, nhưng ở đây chúng quá nhiều và không thực tế lắm.
    • Tham khảo thêm thì có vẻ bản gốc của Jeff Dean được viết từ trước rất lâu chứ không phải năm 2012.
      Đâ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.

    • Nhưng kiến thức tổng quát về công cụ mình dùng lúc nào cũng có giá trị. Nó là tài sản trí tuệ và trong một số tình huống cụ thể có thể rất hữu ích.
    • Khi chạm giới hạn, bạn có thể tìm module viết bằng C hoặc tự viết. Python vốn đã phát triển theo cách đó.
    • Tôi cũng thường làm việc với cảm giác “đủ nhanh” trong hầu hết trường hợp. Tài liệu lần này giúp tôi xác nhận cảm giác đó bằng con số.
  • 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.

    • Tôi nhớ tới câu đùa kiểu bệnh nhân nói với bác sĩ “làm thế này thì đau”, bác sĩ đáp “vậy thì đừng làm nữa”.
      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.
    • Bản thân việc tạo class instance thường không phải vấn đề hiệu năng.
      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)bằng nhau (==).

    • Java cũng có hành vi tương tự. Với người mới thì điều này có thể gây bối rối.