Có giới hạn kỹ thuật hoặc tính năng ngôn ngữ nào ngăn tập lệnh Python của tôi nhanh như chương trình C ++ tương đương không?


10

Tôi là người dùng Python lâu năm. Vài năm trước, tôi bắt đầu học C ++ để xem những gì nó có thể cung cấp về tốc độ. Trong thời gian này, tôi sẽ tiếp tục sử dụng Python như một công cụ để tạo mẫu. Dường như, đây là một hệ thống tốt: phát triển nhanh với Python, thực thi nhanh trong C ++.

Gần đây, tôi đã sử dụng Python nhiều lần nữa và học cách tránh tất cả những cạm bẫy và chống mẫu mà tôi đã nhanh chóng sử dụng trong những năm đầu tiên với ngôn ngữ này. Theo hiểu biết của tôi, việc sử dụng các tính năng nhất định (hiểu danh sách, liệt kê, v.v.) có thể tăng hiệu suất.

Nhưng có giới hạn kỹ thuật hoặc tính năng ngôn ngữ nào ngăn tập lệnh Python của tôi nhanh như chương trình C ++ tương đương không?


2
Vâng, nó có thể. Xem PyPy để biết trạng thái của nghệ thuật trong trình biên dịch Python.
Greg Hewgill

5
Tất cả các biến trong python là đa hình có nghĩa là loại biến chỉ được biết khi chạy. Nếu bạn thấy (giả sử số nguyên) x + y trong các ngôn ngữ giống như C, họ thực hiện phép cộng số nguyên. Trong python sẽ có một công tắc trên các loại biến trên x và y và sau đó chức năng bổ sung thích hợp được chọn và sau đó sẽ có kiểm tra tràn và sau đó có bổ sung. Trừ khi con trăn học cách gõ tĩnh, chi phí này sẽ không bao giờ biến mất.
nwp

1
@nwp Không, thật dễ dàng, xem PyPy. Trickier, vẫn mở, các vấn đề bao gồm: Cách khắc phục độ trễ khởi động của trình biên dịch JIT, cách tránh phân bổ cho các biểu đồ đối tượng tồn tại lâu phức tạp và cách sử dụng tốt bộ đệm nói chung.

Câu trả lời:


11

Tôi đã tự mình đánh vào bức tường này khi tôi làm một công việc lập trình Python toàn thời gian vài năm trước. Tôi yêu Python, tôi thực sự làm, nhưng khi tôi bắt đầu thực hiện một số điều chỉnh hiệu suất, tôi đã có một số cú sốc thô lỗ.

Pythonistas nghiêm ngặt có thể sửa chữa tôi, nhưng đây là những điều tôi tìm thấy, được vẽ bằng những nét rất rộng.

  • Sử dụng bộ nhớ Python là loại đáng sợ. Python đại diện cho mọi thứ như một dict - cực kỳ mạnh mẽ, nhưng có một kết quả là ngay cả các loại dữ liệu đơn giản cũng là khổng lồ. Tôi nhớ ký tự "a" chiếm 28 byte bộ nhớ. Nếu bạn đang sử dụng các cấu trúc dữ liệu lớn trong Python, hãy đảm bảo dựa vào numpy hoặc scipy, bởi vì chúng được hỗ trợ bởi việc thực hiện mảng byte trực tiếp.

Điều đó có tác động đến hiệu suất, bởi vì điều đó có nghĩa là có thêm các mức độ gián tiếp trong thời gian chạy, ngoài việc lấp đầy xung quanh số lượng bộ nhớ lớn so với các ngôn ngữ khác.

  • Python có khóa trình thông dịch toàn cầu, điều đó có nghĩa là đối với hầu hết các phần, các tiến trình đang chạy một luồng đơn. Có thể có các thư viện phân phối các tác vụ qua các quy trình, nhưng chúng tôi đã quay vòng 32 hoặc hơn các tập lệnh python của chúng tôi và chạy từng luồng đơn.

Những người khác có thể nói chuyện với mô hình thực thi, nhưng Python là một trình biên dịch tại thời gian chạy và sau đó được giải thích, điều đó có nghĩa là nó không đi đến mã máy. Điều đó cũng có tác động hiệu suất. Bạn có thể dễ dàng liên kết trong các mô-đun C hoặc C ++ hoặc tìm thấy chúng, nhưng nếu bạn chỉ cần chạy thẳng lên Python, nó sẽ có hiệu năng.

Bây giờ, trong các điểm chuẩn dịch vụ web, Python so sánh thuận lợi với các ngôn ngữ biên dịch tại thời gian chạy khác như Ruby hoặc PHP. Nhưng nó khá xa so với hầu hết các ngôn ngữ được biên dịch. Ngay cả các ngôn ngữ biên dịch sang ngôn ngữ trung gian và chạy trong VM (như Java hoặc C #) cũng làm được nhiều điều tốt hơn nhiều.

Đây là một bộ kiểm tra điểm chuẩn thực sự thú vị mà tôi thỉnh thoảng nhắc đến:

http://www.techempower.com/benchmark/

(Tất cả những gì đã nói, tôi vẫn yêu Python, và nếu tôi có cơ hội chọn ngôn ngữ tôi đang làm việc, đó là lựa chọn đầu tiên của tôi. Hầu hết, tôi không bị hạn chế bởi các yêu cầu thông lượng điên rồ nào.)


2
Chuỗi "a" không phải là một ví dụ tốt cho điểm đầu tiên. Một chuỗi Java cũng có chi phí đáng kể cho các chuỗi ký tự đơn, nhưng đó là chi phí không đổi được phân bổ khá tốt khi chuỗi phát triển theo chiều dài (một đến bốn byte ký tự oer tùy thuộc vào phiên bản, tùy chọn xây dựng và nội dung chuỗi). Tuy nhiên, bạn đúng về các đối tượng do người dùng xác định, ít nhất là các đối tượng không sử dụng __slots__. PyPy nên làm tốt hơn nhiều về vấn đề này nhưng tôi không biết đủ để phán xét.

1
Vấn đề thứ hai bạn chỉ ra chỉ liên quan đến việc triển khai cụ thể và không liên quan đến ngôn ngữ. Vấn đề đầu tiên yêu cầu giải thích: những gì "nặng" 28 byte không phải là bản thân ký tự mà thực tế là nó được gói trong một lớp chuỗi, đi kèm với các phương thức và thuộc tính của chính nó. Đại diện cho một ký tự là mảng byte (chữ b'a ') "chỉ" nặng 18 byte trên Python 3.3 và tôi chắc chắn có nhiều cách để tối ưu hóa lưu trữ ký tự trong bộ nhớ nếu ứng dụng của bạn thực sự cần nó.
Đỏ

C # có thể biên dịch nguyên bản (ví dụ: MS tech, Xamarin cho iOS).
Den

13

Việc triển khai tham chiếu Python là trình thông dịch CP CPython. Nó cố gắng nhanh chóng một cách hợp lý, nhưng hiện tại nó không sử dụng tối ưu hóa nâng cao. Và đối với nhiều kịch bản sử dụng, đây là một điều tốt: Việc biên dịch một số mã trung gian xảy ra ngay trước khi chạy và mỗi khi chương trình được thực thi, mã sẽ được biên dịch lại. Vì vậy, thời gian cần thiết để tối ưu hóa phải được cân nhắc với thời gian đạt được bằng cách tối ưu hóa - nếu không có lợi ích ròng, thì việc tối ưu hóa là vô ích. Đối với một chương trình rất dài hoặc một chương trình có các vòng lặp rất chặt chẽ, sử dụng tối ưu hóa nâng cao sẽ hữu ích. Tuy nhiên, CPython được sử dụng cho một số công việc ngăn chặn tối ưu hóa mạnh mẽ:

  • Các tập lệnh chạy ngắn, được sử dụng, ví dụ như cho các tác vụ sysadmin. Nhiều hệ điều hành như Ubuntu xây dựng một phần cơ sở hạ tầng tốt trên Python: CPython đủ nhanh cho công việc, nhưng hầu như không có thời gian khởi động. Miễn là nó nhanh hơn bash, thì tốt.

  • CPython phải có ngữ nghĩa rõ ràng, vì nó là một triển khai tham chiếu. Điều này cho phép tối ưu hóa đơn giản, chẳng hạn như tối ưu hóa việc triển khai toán tử biên dịch foo của nhà điều hành foo hoặc mã hóa để hiểu nhanh hơn bằng cách mã hóa, nhưng nói chung sẽ loại trừ tối ưu hóa phá hủy thông tin, chẳng hạn như các hàm nội tuyến.

Tất nhiên, có nhiều triển khai Python hơn là CPython:

  • Jython được xây dựng trên JVM. JVM có thể diễn giải hoặc biên dịch JIT mã byte được cung cấp và có các tối ưu hóa theo hướng dẫn hồ sơ. Nó phải chịu đựng thời gian khởi động cao, và phải mất một thời gian cho đến khi JIT khởi động.

  • PyPy là một công nghệ tiên tiến, JITting Python VM. PyPy được viết bằng RPython, một tập hợp con bị hạn chế của Python. Tập hợp con này loại bỏ một số biểu cảm khỏi Python, nhưng cho phép loại bất kỳ biến nào được suy ra tĩnh. VM được viết bằng RPython sau đó có thể được dịch sang C, mang lại hiệu năng giống như RPython. Tuy nhiên, RPython vẫn biểu cảm hơn C, cho phép phát triển nhanh hơn các tối ưu hóa mới. PyPy là một ví dụ về trình biên dịch bootstrapping. PyPy (không phải RPython!) Hầu hết tương thích với việc triển khai tham chiếu CPython.

  • Cython là (như RPython) là một phương ngữ Python không tương thích với kiểu gõ tĩnh. Nó cũng dịch mã sang mã C và có thể dễ dàng tạo các phần mở rộng C cho trình thông dịch CPython.

Nếu bạn sẵn sàng dịch mã Python của mình sang Cython hoặc RPython, thì bạn sẽ có được hiệu suất như C. Tuy nhiên, chúng không nên được hiểu là một tập hợp con của Python, mà là một tên C với cú pháp Pythonic. Nếu bạn chuyển sang PyPy, mã Python vanilla của bạn sẽ được tăng tốc đáng kể, nhưng cũng sẽ không thể giao tiếp với các tiện ích mở rộng được viết bằng C hoặc C ++.

Nhưng những tính chất hoặc tính năng nào ngăn vanilla Python đạt được mức hiệu suất như C, ngoài thời gian khởi động dài?

  • Người đóng góp và tài trợ. Không giống như Java hay C #, không có công ty lái xe nào đứng sau ngôn ngữ này để làm cho ngôn ngữ này trở thành tốt nhất trong lớp. Điều này hạn chế sự phát triển chủ yếu cho các tình nguyện viên và các khoản trợ cấp không thường xuyên.

  • Ràng buộc muộn và thiếu bất kỳ gõ tĩnh. Python cho phép chúng ta viết crap như thế này:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding

    Trong Python, bất kỳ biến nào cũng có thể được gán lại bất cứ lúc nào. Điều này ngăn chặn bộ nhớ đệm hoặc nội tuyến; bất kỳ truy cập phải đi qua các biến. Sự thiếu quyết đoán này làm giảm hiệu suất. Tất nhiên: nếu mã của bạn không làm những điều điên rồ như vậy để mỗi biến có thể được cung cấp một kiểu dứt khoát trước khi biên dịch và mỗi biến chỉ được gán một lần, thì - theo lý thuyết - một mô hình thực thi hiệu quả hơn có thể được chọn. Một ngôn ngữ có ý nghĩa này sẽ cung cấp một số cách để đánh dấu các định danh là hằng số, và ít nhất là cho phép các chú thích loại tùy chọn (gõ từ từ gõ).

  • Một mô hình đối tượng nghi vấn. Trừ khi các vị trí được sử dụng, thật khó để tìm ra trường nào có một đối tượng (đối tượng Python thực chất là bảng băm của các trường). Và ngay cả khi chúng tôi ở đó, chúng tôi vẫn không biết những loại này có những loại nào. Điều này ngăn việc biểu diễn các đối tượng dưới dạng các cấu trúc được đóng gói chặt chẽ, như trường hợp trong C ++. (Tất nhiên, đại diện cho các đối tượng của C ++ cũng không lý tưởng: do tính chất giống cấu trúc, ngay cả các trường riêng cũng thuộc về giao diện chung của một đối tượng.)

  • Thu gom rác thải. Trong nhiều trường hợp, GC có thể tránh hoàn toàn. C ++ cho phép chúng ta phân bổ tĩnh các đối tượng bị hủy tự động khi phạm vi hiện tại còn lại : Type instance(args);. Cho đến lúc đó, đối tượng còn sống và có thể được cho mượn các chức năng khác. Điều này thường được thực hiện thông qua thông qua tham khảo qua Pass. Các ngôn ngữ như Rust cho phép trình biên dịch kiểm tra tĩnh rằng không có con trỏ nào cho một đối tượng như vậy vượt quá vòng đời của đối tượng. Sơ đồ quản lý bộ nhớ này hoàn toàn có thể dự đoán được, hiệu quả cao và phù hợp với hầu hết các trường hợp mà không có biểu đồ đối tượng phức tạp. Thật không may, Python không được thiết kế để quản lý bộ nhớ. Về lý thuyết, phân tích thoát có thể được sử dụng để tìm các trường hợp có thể tránh được GC. Trong thực tế, các chuỗi phương thức đơn giản nhưfoo().bar().baz() sẽ phải phân bổ một số lượng lớn các đối tượng tồn tại ngắn trên heap (GC thế hệ là một cách để giữ cho vấn đề này nhỏ).

    Trong các trường hợp khác, lập trình viên có thể đã biết kích thước cuối cùng của một số đối tượng, chẳng hạn như danh sách. Thật không may, Python không cung cấp một cách để truyền đạt điều này khi tạo một danh sách mới. Thay vào đó, các mục mới sẽ được đẩy lên cuối, có thể yêu cầu nhiều phân bổ lại. Một vài lưu ý:

    • Danh sách kích thước cụ thể có thể được tạo ra như thế nào fixed_size = [None] * size. Tuy nhiên, bộ nhớ cho các đối tượng trong danh sách đó sẽ phải được phân bổ riêng biệt. Tương phản C ++, nơi chúng ta có thể làm std::array<Type, size> fixed_size.

    • Các mảng được đóng gói của một kiểu gốc cụ thể có thể được tạo trong Python thông qua arraymô đun dựng sẵn. Ngoài ra, numpycung cấp các biểu diễn hiệu quả của bộ đệm dữ liệu với hình dạng cụ thể cho các kiểu số gốc.

Tóm lược

Python được thiết kế để dễ sử dụng, không phải cho hiệu suất. Thiết kế của nó làm cho việc thực hiện hiệu quả cao khá khó khăn. Nếu lập trình viên tránh các tính năng có vấn đề, thì trình biên dịch hiểu các thành ngữ còn lại sẽ có thể phát ra mã hiệu quả có thể cạnh tranh với C về hiệu suất.


8

Đúng. Vấn đề chính là ngôn ngữ được xác định là động - nghĩa là bạn không bao giờ biết mình đang làm gì cho đến khi bạn chuẩn bị làm điều đó. Điều đó khiến cho việc sản xuất mã máy hiệu quả rất khó khăn, vì bạn không biết sản xuất mã máy để làm gì . Trình biên dịch JIT có thể thực hiện một số công việc trong lĩnh vực này nhưng nó không bao giờ có thể so sánh với C ++ vì trình biên dịch JIT đơn giản là không thể dành thời gian và bộ nhớ để chạy, vì đó là thời gian và bộ nhớ mà bạn không dành để chạy chương trình của mình và có những giới hạn cứng đối với những gì họ có thể đạt được mà không phá vỡ ngữ nghĩa ngôn ngữ động.

Tôi sẽ không tuyên bố rằng đây là một sự đánh đổi không thể chấp nhận được. Nhưng điều cơ bản đối với bản chất của Python là việc triển khai thực sự sẽ không bao giờ nhanh như việc triển khai C ++.


8

Có ba yếu tố chính ảnh hưởng đến hiệu suất của tất cả các ngôn ngữ động, một số yếu tố khác.

  1. Giải thích trên cao. Trong thời gian chạy có một số loại mã byte thay vì hướng dẫn máy và có một chi phí cố định để thực thi mã này.
  2. Công văn trên cao. Mục tiêu cho một cuộc gọi chức năng không được biết cho đến khi thực thi và việc tìm ra phương thức nào để gọi mang chi phí.
  3. Quản lý bộ nhớ trên cao. Ngôn ngữ động lưu trữ nội dung trong các đối tượng phải được phân bổ và phân bổ, và điều đó mang lại hiệu suất cao.

Đối với C / C ++, chi phí tương đối của 3 yếu tố này gần như bằng không. Các hướng dẫn được thực thi trực tiếp bởi bộ xử lý, công văn mất tối đa một hoặc hai lần, bộ nhớ heap không bao giờ được phân bổ trừ khi bạn nói như vậy. Mã được viết tốt có thể tiếp cận ngôn ngữ lắp ráp.

Đối với C # / Java với trình biên dịch JIT, hai phần đầu tiên thấp nhưng bộ nhớ được thu gom rác có chi phí. Mã được viết tốt có thể tiếp cận 2x C / C ++.

Đối với Python / Ruby / Perl, chi phí của cả ba yếu tố này là tương đối cao. Hãy nghĩ 5x so với C / C ++ hoặc tệ hơn. (*)

Hãy nhớ rằng mã thư viện thời gian chạy cũng có thể được viết bằng cùng ngôn ngữ với các chương trình của bạn và có cùng giới hạn hiệu suất.


(*) Khi quá trình biên dịch Just-In_Time (JIT) được mở rộng sang các ngôn ngữ này, chúng cũng sẽ tiếp cận (thường là 2 lần) tốc độ của mã C / C ++ được viết tốt.

Cũng cần lưu ý rằng một khi khoảng cách là hẹp (giữa các ngôn ngữ cạnh tranh), thì sự khác biệt bị chi phối bởi các thuật toán và chi tiết triển khai. Mã JIT có thể đánh bại C / C ++ và C / C ++ có thể đánh bại ngôn ngữ lắp ráp vì việc viết mã tốt sẽ dễ dàng hơn.


"Hãy nhớ rằng mã thư viện thời gian chạy cũng có thể được viết bằng cùng ngôn ngữ với các chương trình của bạn và có cùng giới hạn hiệu suất." và "Đối với Python / Ruby / Perl, chi phí của cả ba yếu tố này tương đối cao. Hãy nghĩ 5x so với C / C ++ hoặc tệ hơn." Trên thực tế đó là không đúng sự thật. Ví dụ, Hashlớp Rubinius (một trong những cơ sở dữ liệu cốt lõi trong Ruby) được viết bằng Ruby và nó hoạt động tương đương, đôi khi còn nhanh hơn cả Hashlớp YARV được viết bằng C. Và một trong những lý do là phần lớn thời gian chạy của Rubinius hệ thống được viết bằng Ruby, để họ có thể
Bẻ khóa

Ví dụ, phần mềm được biên dịch bởi trình biên dịch Rubinius. Các ví dụ cực đoan là Klein VM (VM siêu vòng tròn cho bản thân) và Maxine VM (VM siêu vòng tròn cho Java), trong đó mọi thứ , ngay cả mã gửi phương thức, trình thu gom rác, cấp phát bộ nhớ, kiểu nguyên thủy, cơ sở dữ liệu lõi và thuật toán được viết trong Tự hoặc Java. Theo cách đó, thậm chí các phần của VM lõi có thể được nhập vào mã người dùng và VM có thể biên dịch lại và tự tối ưu hóa lại bằng cách sử dụng phản hồi thời gian chạy từ chương trình người dùng.
Jörg W Mittag

@ JörgWMittag: Vẫn đúng. Rubinius có JIT và mã JIT thường đánh bại C / C ++ trên các điểm chuẩn riêng lẻ. Tôi không thể tìm thấy bất kỳ bằng chứng nào cho thấy công cụ siêu vòng tròn này ảnh hưởng nhiều đến tốc độ khi không có JIT. [Xem chỉnh sửa để rõ ràng về JIT.]
david.pfx

1

Nhưng có giới hạn kỹ thuật hoặc tính năng ngôn ngữ nào ngăn tập lệnh Python của tôi nhanh như chương trình C ++ tương đương không?

Không. Đó chỉ là một câu hỏi về tiền bạc và tài nguyên đổ vào khiến C ++ chạy nhanh so với tiền và tài nguyên đổ vào khiến Python chạy nhanh.

Ví dụ, khi Self VM xuất hiện, nó không chỉ là ngôn ngữ OO động nhanh nhất, nó là thời kỳ ngôn ngữ OO nhanh nhất. Mặc dù là một ngôn ngữ cực kỳ năng động (chẳng hạn như nhiều hơn Python, Ruby, PHP hoặc JavaScript), nhưng nó nhanh hơn hầu hết các triển khai C ++ có sẵn.

Nhưng sau đó, Sun đã hủy dự án Self (một ngôn ngữ OO có mục đích chung để phát triển các hệ thống lớn) để tập trung vào một ngôn ngữ kịch bản nhỏ cho các menu hoạt hình trong các hộp trên TV (bạn có thể đã nghe về nó, nó được gọi là Java), không có tài trợ nhiều hơn. Đồng thời, Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. đã chi rất nhiều tiền và tài nguyên để làm cho C ++ nhanh chóng. Các nhà sản xuất CPU đã thêm các tính năng vào chip của họ để làm cho C ++ nhanh chóng. Hệ điều hành đã được viết hoặc sửa đổi để làm cho C ++ nhanh chóng. Vì vậy, C ++ là nhanh chóng.

Tôi không quen thuộc lắm với Python, tôi là một người Ruby hơn, vì vậy tôi sẽ đưa ra một ví dụ từ Ruby: Hashlớp (tương đương về chức năng và tầm quan trọng của dictPython) trong triển khai Ruby của Rubinius được viết bằng Ruby thuần túy 100%; Tuy nhiên, nó cạnh tranh thuận lợi và đôi khi còn vượt trội so với Hashlớp trong YARV được viết bằng tay C. được tối ưu hóa và so với một số hệ thống Lisp hoặc Smalltalk thương mại (hoặc Self VM được đề cập ở trên), trình biên dịch của Rubinius thậm chí còn không thông minh .

Không có gì vốn có trong Python làm cho nó chậm. Có những tính năng trong bộ xử lý và hệ điều hành ngày nay làm tổn thương Python (ví dụ bộ nhớ ảo được biết là khủng khiếp đối với hiệu suất thu gom rác). Có những tính năng giúp C ++ nhưng không giúp Python (CPU hiện đại cố gắng tránh bỏ lỡ bộ đệm, vì chúng rất tốn kém. Thật không may, tránh bỏ lỡ bộ nhớ cache là khó khăn khi bạn có OO và đa hình. bỏ lỡ. CPU Azul Vega, được thiết kế cho Java, thực hiện điều này.)

Nếu bạn chi nhiều tiền, nghiên cứu và tài nguyên để tạo Python nhanh, như đã làm cho C ++ và bạn dành nhiều tiền, nghiên cứu và tài nguyên cho việc tạo các hệ điều hành làm cho các chương trình Python chạy nhanh như đã làm cho C ++ và bạn chi tiêu như nhiều tiền, nghiên cứu và tài nguyên để tạo ra các CPU làm cho các chương trình Python chạy nhanh như đã làm cho C ++, không còn nghi ngờ gì nữa, tôi nghĩ rằng Python có thể đạt được hiệu năng tương đương với C ++.

Chúng tôi đã thấy với ECMAScript những gì có thể xảy ra nếu chỉ một người chơi nghiêm túc về hiệu suất. Trong vòng một năm, về cơ bản, chúng tôi đã tăng hiệu suất gấp 10 lần cho tất cả các nhà cung cấp lớn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.