C ++ đến C chậm hơn bao nhiêu?

Trong bài viết này, chúng ta sẽ khám phá ra rằng Python không phải là một ngôn ngữ tồi mà chỉ là rất chậm. Nó được tối ưu hóa cho mục đích nó được xây dựng. cú pháp dễ dàng, mã dễ đọc và rất nhiều tự do cho nhà phát triển. Tuy nhiên, những lựa chọn thiết kế này làm cho mã Python chậm hơn các ngôn ngữ khác như C và Java

Hiểu cách Python hoạt động bí mật sẽ cho chúng ta thấy nguyên nhân tại sao nó chậm hơn. Một khi các nguyên nhân đã rõ ràng, chúng ta có thể giải quyết vấn đề theo cách của mình. Sau khi đọc bài viết này, bạn sẽ hiểu rõ về

  • cách Python được thiết kế và hoạt động bí mật
  • tại sao các lựa chọn thiết kế này lại ảnh hưởng đến tốc độ thực thi
  • cách chúng tôi có thể khắc phục một số tắc nghẽn này để tăng tốc độ mã của chúng tôi một cách đáng kể

Bài viết này được chia thành ba phần. Trong phần A, chúng ta hãy xem Python được thiết kế như thế nào. Sau đó, trong phần B, hãy xem cách thức và lý do tại sao các lựa chọn thiết kế này ảnh hưởng đến tốc độ. Cuối cùng, trong phần C, chúng ta sẽ tìm hiểu cách khắc phục các nút cổ chai do thiết kế của Python và cách chúng ta có thể tăng tốc đáng kể mã của mình
Đi nào

Phần A - Thiết kế của Python

Hãy bắt đầu với một định nghĩa. Wikipedia mô tả Python là

Python là một ngôn ngữ lập trình thông dịch, cấp cao, có mục đích chung. Nó được gõ động và thu gom rác

Tin hay không thì tùy, bạn sẽ hiểu hai câu trên sau khi đọc bài viết này. Định nghĩa này cung cấp một cái nhìn thoáng qua về thiết kế của Python. Gõ năng động, thông dịch, có mục đích chung, cấp cao và cách thức thu gom rác loại bỏ rất nhiều rắc rối từ nhà phát triển

Trong các phần tiếp theo, chúng ta sẽ xem xét các yếu tố thiết kế này, giải thích ý nghĩa của nó đối với hiệu suất của Python và kết luận bằng một ví dụ thực tế

Trăn trở như một con diều; . C giống như một chiếc máy bay chiến đấu;

Chậm so với chờ đợi

Trước tiên, hãy nói về những gì chúng tôi đang cố gắng đo lường khi chúng tôi nói “chậm”. Mã của bạn có thể bị chậm vì vô số lý do nhưng không phải tất cả đều là lỗi của Python. Giả sử có hai loại nhiệm vụ

  1. I/O-tác vụ
  2. tác vụ CPU

Ví dụ về các tác vụ I/O đang viết một tệp, yêu cầu một số dữ liệu từ API, in một trang; . Mặc dù chúng khiến chương trình của bạn mất nhiều thời gian hơn để thực thi nhưng đây không phải là lỗi của Python. Nó chỉ đang chờ phản hồi; . Loại chậm chạp này không phải là điều chúng tôi đang cố gắng giải quyết trong bài viết này. Như chúng ta sẽ thấy ở phần sau, chúng ta có thể xâu chuỗi các loại tác vụ này (cũng được mô tả trong bài viết này)

Trong bài viết này, chúng tôi tìm hiểu lý do tại sao Python thực thi các tác vụ CPU chậm hơn các ngôn ngữ khác

Nhập động so với nhập tĩnh

Python được gõ động. Trong các ngôn ngữ như C, Java hoặc C++, tất cả các biến đều được nhập tĩnh, điều này có nghĩa là bạn viết ra loại biến cụ thể như int my_var = 1;
Trong Python, chúng ta chỉ cần gõ my_var = 1. Sau đó, chúng tôi thậm chí có thể gán một giá trị mới thuộc loại hoàn toàn khác như my_var = “a string". Chúng ta sẽ thấy nó hoạt động như thế nào trong chương tiếp theo

Mặc dù gõ động khá dễ dàng đối với nhà phát triển, nhưng nó có một số nhược điểm lớn như chúng ta sẽ thấy trong các phần tiếp theo

Biên dịch so với giải thích

Biên dịch mã có nghĩa là lấy một chương trình bằng một ngôn ngữ và chuyển đổi nó sang ngôn ngữ khác, thường là cấp độ thấp hơn nguồn. Khi bạn biên dịch một chương trình viết bằng C, bạn chuyển đổi mã nguồn thành mã máy (là các lệnh thực tế cho CPU), sau đó bạn có thể chạy chương trình của mình

Python hoạt động hơi khác một chút

  1. Mã nguồn không được biên dịch thành mã máy mà thành mã byte độc ​​lập với nền tảng. Giống như mã máy, bytecode cũng là các lệnh nhưng thay vì được thực thi bởi CPU, chúng được thực thi bởi một trình thông dịch
  2. Mã nguồn được biên dịch trong khi chạy. Python đã biên dịch các tệp khi cần thay vì biên dịch mọi thứ trước khi chạy chương trình
  3. Trình thông dịch phân tích bytecode và dịch nó sang mã máy

Python phải biên dịch thành mã byte vì nó được nhập động. Bởi vì chúng tôi không chỉ định trước loại biến, chúng tôi phải đợi giá trị thực để xác định xem những gì chúng tôi đang cố gắng thực hiện có thực sự hợp pháp hay không (như cộng hai số nguyên) trước khi dịch sang mã máy. Đây là những gì thông dịch viên làm. Trong các ngôn ngữ được biên dịch, được gõ tĩnh, quá trình biên dịch và diễn giải diễn ra trước khi chạy mã

Tóm tắt. mã bị chậm lại do quá trình biên dịch và diễn giải xảy ra trong thời gian chạy. So sánh ngôn ngữ này với một ngôn ngữ được biên dịch, được gõ tĩnh, chỉ chạy các lệnh CPU sau khi được biên dịch

Thực sự có thể mở rộng Python bằng các mô-đun đã biên dịch được viết bằng C. Bài viết này và bài viết này trình bày cách bạn có thể viết mã phần mở rộng của riêng mình bằng C để tăng tốc mã x100

Thu gom rác và quản lý bộ nhớ

Khi bạn tạo một biến trong Python, trình thông dịch sẽ tự động chọn một vị trí trong bộ nhớ đủ lớn cho giá trị của biến và lưu nó ở đó. Sau đó, khi biến không cần thiết nữa, khe bộ nhớ sẽ được giải phóng lại để các quy trình khác có thể sử dụng lại

Trong C, ngôn ngữ mà Python được viết, quá trình này hoàn toàn không tự động. Khi bạn khai báo một biến, bạn cần chỉ định loại của nó để có thể phân bổ đúng dung lượng bộ nhớ. Ngoài ra thu gom rác là thủ công

Vậy làm cách nào để Python theo dõi biến nào cần thu gom rác? . Nếu số lượng tham chiếu của một biến là 0 thì chúng ta có thể kết luận rằng biến đó không được sử dụng và nó có thể được giải phóng trong bộ nhớ. Chúng ta sẽ thấy điều này trong hành động trong chương tiếp theo

Đơn luồng so với đa luồng

Một số ngôn ngữ, như Java, cho phép bạn chạy mã song song trên nhiều CPU. Tuy nhiên, Python là đơn luồng trên một CPU theo thiết kế. Cơ chế đảm bảo điều này được gọi là GIL. Khóa phiên dịch viên toàn cầu. GIL đảm bảo rằng trình thông dịch chỉ thực thi một luồng tại bất kỳ thời điểm nào

Vấn đề mà GIL giải quyết là cách Python sử dụng tính tham chiếu để quản lý bộ nhớ. Số lượng tham chiếu của một biến cần được bảo vệ khỏi các tình huống trong đó hai luồng đồng thời tăng hoặc giảm số lượng. Điều này có thể gây ra tất cả các loại lỗi kỳ lạ dẫn đến rò rỉ bộ nhớ (khi một đối tượng không còn cần thiết nhưng không bị xóa) hoặc tệ hơn là giải phóng bộ nhớ không chính xác. Trong trường hợp cuối cùng, một biến bị xóa khỏi bộ nhớ trong khi các biến khác vẫn cần đến nó

Nói ngắn gọn. Do cách thiết kế bộ sưu tập rác, Python phải triển khai GIL để đảm bảo nó chạy trên một luồng đơn. Tuy nhiên, có nhiều cách để vượt qua GIL, hãy đọc bài viết này, để phân luồng hoặc đa xử lý mã của bạn và tăng tốc đáng kể

Phần B - Một cái nhìn sâu sắc. Thiết kế Pythons trong thực tế

Lý thuyết đủ rồi, hãy xem hành động nào. Bây giờ chúng ta đã biết Python được thiết kế như thế nào, hãy xem nó hoạt động như thế nào. Chúng ta sẽ so sánh cách khai báo đơn giản của một biến trong cả C và Python. Bằng cách này, chúng ta có thể thấy cách Python quản lý bộ nhớ của nó và lý do tại sao các lựa chọn thiết kế của nó dẫn đến thời gian thực thi chậm so với C

Bây giờ chúng ta đã giải mã hoàn toàn Python, hãy đặt nó trở lại với nhau và kiểm tra xem nó chạy như thế nào (hình ảnh của Jordan Bebek trên Bapt)

Khai báo biến trong C

Hãy bắt đầu bằng cách khai báo một số nguyên trong C có tên là c_num

int c_num = 42;

Khi chúng tôi thực thi dòng mã này, máy của chúng tôi sẽ thực hiện như sau

  1. Cấp phát đủ bộ nhớ cho một số nguyên tại một địa chỉ nhất định (vị trí trong bộ nhớ)
  2. Gán giá trị 42 cho vị trí của bộ nhớ được cấp phát ở bước trước
  3. Trỏ c_num tới giá trị đó

Hình ảnh hiện tồn tại một đối tượng trong bộ nhớ trông như thế này

Biểu diễn biến số nguyên gọi là c_num với giá trị 42 (ảnh của tác giả)

Nếu chúng ta gán một số mới cho c_num, chúng ta sẽ ghi số mới vào cùng một địa chỉ; . Điều này có nghĩa là biến có thể thay đổi

Chúng tôi đã gán giá trị 404 cho c_num (hình ảnh của tác giả)

Lưu ý rằng địa chỉ (hoặc vị trí trong bộ nhớ) không thay đổi. Hãy coi đó là c_num sở hữu một phần bộ nhớ đủ lớn cho một số nguyên. Bạn sẽ thấy trong phần tiếp theo rằng điều này khác với cách thức hoạt động của Python

Khai báo một biến trong Python

Chúng tôi sẽ làm điều tương tự như trong phần trước;

py_num = 42

Dòng mã này khởi động các bước sau trong quá trình thực thi

  1. Tạo một PyObject;
  2. Đặt mã kiểu của PyObject thành số nguyên (do trình thông dịch xác định)
  3. Đặt giá trị của PyObject thành 42
  4. Tạo một tên gọi là py_num
  5. Trỏ py_num tới Pyobject
  6. Tăng số lượng truy cập của PyObject lên 1

Dưới mui xe, điều đầu tiên được thực hiện là tạo một PyObject. Đây là ý nghĩa của cụm từ 'mọi thứ trong Python đều là một đối tượng'. Python có thể có các loại int, strfloat nhưng về cơ bản, mọi biến Python chỉ là một PyObject. Đây là lý do tại sao có thể gõ động

Lưu ý rằng PyObject không phải là một đối tượng trong Python. Đó là một cấu trúc trong C đại diện cho tất cả các đối tượng Python. Nếu bạn quan tâm đến cách PyObject này hoạt động trong C, hãy xem bài viết này nơi chúng tôi viết mã phần mở rộng C của riêng mình bằng Python để tăng tốc độ thực thi x100

Các bước trên tạo các đối tượng (đơn giản hóa) trong bộ nhớ bên dưới

Số nguyên Python của chúng tôi trong bộ nhớ (đơn giản hóa) (hình ảnh của Tác giả)

Bạn sẽ ngay lập tức nhận thấy rằng chúng tôi thực hiện nhiều bước hơn và cần nhiều bộ nhớ hơn để lưu trữ một số nguyên. Ngoài loại và giá trị, chúng tôi cũng lưu trữ số lần đếm lại cho mục đích thu gom rác. Ngoài ra, bạn sẽ nhận thấy rằng biến chúng tôi đã tạo, py_num, không sở hữu khối bộ nhớ. Bộ nhớ được sở hữu bởi PyObject mới được tạo mà py_num trỏ tới

Về mặt kỹ thuật, Python không có biến như C; . Các biến sở hữu các phần bộ nhớ và có thể được ghi đè, tên là con trỏ tới một biến

Vậy điều gì sẽ xảy ra khi chúng ta muốn gán một giá trị khác cho py_num?

  1. Tạo một PyObject mới tại một địa chỉ nhất định, cấp phát đủ bộ nhớ
  2. Đặt mã kiểu của PyObject thành số nguyên
  3. Đặt giá trị của PyObject thành 404 (giá trị mới)
  4. Trỏ py_num tới Pyobject
  5. Tăng số lượng truy cập của PyObject mới lên 1
  6. Giảm refcount của PyObject cũ đi 1

Các bước này dẫn đến thay đổi bộ nhớ như trong hình bên dưới

Bộ nhớ của chúng ta sau khi gán giá trị mới cho py_num (ảnh của tác giả)

Hình ảnh trên sẽ chứng minh rằng thay vì gán một giá trị mới cho py_num, thay vào đó, chúng tôi liên kết tên py_num với một đối tượng mới. Bằng cách này, chúng ta cũng có thể gán một giá trị thuộc loại khác vì một PyObject mới sẽ được tạo mỗi lần. Py_num chỉ trỏ đến một PyObject khác. Chúng tôi không ghi đè như trong C, chúng tôi chỉ trỏ đến một đối tượng khác

Cũng lưu ý rằng số lần đếm ngược trên đối tượng cũ là 0;

Phần C - Làm thế nào để tăng tốc mọi thứ

Trong các phần trước, chúng ta đã tìm hiểu sâu về thiết kế của Pythons và đã thấy hậu quả trong hành động. Chúng ta có thể kết luận rằng các vấn đề chính đối với tốc độ thực thi là

  • Diễn dịch. quá trình biên dịch và giải thích xảy ra trong thời gian chạy do cách nhập động của các biến. Vì lý do tương tự, chúng tôi phải tạo một PyObject mới, chọn một địa chỉ trong bộ nhớ và cấp phát đủ bộ nhớ mỗi khi chúng tôi tạo hoặc “ghi đè” một “biến”, chúng tôi tạo một PyObject mới để cấp phát bộ nhớ
  • chủ đề duy nhất. Cách thức thu gom rác được thiết kế buộc một GIL. giới hạn tất cả việc thực thi thành một luồng trên một CPU

Đã đến lúc tăng tốc độ thối rữa của chúng ta với động cơ phản lực này (hình ảnh của Kaspars Eglitis trên Bapt)

Vì vậy, với tất cả kiến ​​​​thức của bài viết này, làm thế nào để chúng ta khắc phục những vấn đề này?

  1. Sử dụng các mô-đun C tích hợp trong Python như range()
  2. Các tác vụ I/O giải phóng GIL để chúng có thể được phân luồng;
  3. Chạy song song các tác vụ CPU bằng đa xử lý (thông tin thêm)
  4. Tạo và nhập mô-đun C của riêng bạn vào Python; . (thông tin)
  5. Không phải là một lập trình viên C có kinh nghiệm? . Nó cung cấp khả năng đọc và cú pháp dễ dàng của Python với tốc độ của C (thêm thông tin)

Bây giờ thật nhanh. (hình ảnh của SpaceX trên Bapt)Kết luận

Nếu bạn vẫn đang đọc thì sự phức tạp và độ dài của bài viết này không làm bạn sợ hãi. Kudo cho bạn. Tôi hy vọng đã làm sáng tỏ cách Python hoạt động bí mật và cách giải quyết các nút thắt cổ chai của nó

Nếu bạn có góp ý/làm rõ xin vui lòng bình luận để tôi có thể cải thiện bài viết này. Trong thời gian chờ đợi, hãy xem các bài viết khác của tôi về tất cả các loại chủ đề liên quan đến lập trình như thế này

C++ chậm hơn bao nhiêu so với C?

Hyperfine cho tôi biết rằng chương trình C++ dựa trên thư viện C++ được tải động mất gần 1 ms thời gian hơn chương trình C.

C đến C ++ nhanh hơn bao nhiêu?

Nói chung, C nhanh hơn bao nhiêu so với C++? . C and C++ are exactly the same in terms of performance and you can achieve near machine-language efficiency in either if you are well-skilled.

C++ có nhanh bằng C không?

Ngôn ngữ C++ là ngôn ngữ lập trình hướng đối tượng và nó hỗ trợ một số tính năng quan trọng như Đa hình, Kiểu dữ liệu trừu tượng, Đóng gói, v.v. Vì nó hỗ trợ hướng đối tượng nên tốc độ nhanh hơn so với ngôn ngữ C .

C# đến C++ chậm hơn bao nhiêu?

C++ được coi là ngôn ngữ mẹ đẻ vì nó biên dịch trực tiếp thành mã máy mà hệ thống bên dưới có thể hiểu được. Trước tiên, C# phải biên dịch sang Ngôn ngữ trung gian của Microsoft (MSIL) trước khi trình biên dịch just-in-time (JIT) tạo mã máy. Vì lý do này, C++ thường nhanh hơn C# .