Hiệu suất của Julia vs Python

Python là ngôn ngữ lập trình có quá nhiều ưu điểm mà chúng ta không cần liệt kê ra. Người ta cũng thường thừa nhận rằng nhược điểm chính của nó là hiệu suất mã.

Trước vấn đề này, nhiều cách tăng tốc Python đã được đề xuất theo thời gian. Mục tiêu của các cách tiếp cận khác nhau là đưa vào một chút mã được biên dịch để làm cho Python nhanh, với ít nỗ lực nhất có thể về phía lập trình viên

Chúng tôi thường chỉ muốn làm điều này cho một phần quan trọng về hiệu năng, trong khi vẫn giữ được lợi thế của logic cấp cao hơn và hệ sinh thái gói phong phú

Có nhiều lựa chọn trong lĩnh vực này, như Cython, PyPy, gọi hàm Fortran/C, v.v. Trong bài đăng này, chúng tôi sẽ chủ yếu tập trung vào Numba, tại thời điểm viết bài này, nó có vẻ đủ trưởng thành cho các trường hợp thử nghiệm thực tế và dường như đang hoạt động tốt, ít nhất là trên một số tiêu chuẩn vi mô.

Tôi thấy rằng việc chạy điểm chuẩn cá nhân có lợi trước khi cam kết lâu dài với một nhóm phần mềm cụ thể. Trong bài đăng này, tôi đang chia sẻ một số kết luận sơ bộ, dựa trên cuộc thảo luận mà tôi tìm thấy trên mạng và bài kiểm tra mà tôi đã đưa ra, để kiểm tra một số ý tưởng đang thảo luận xung quanh những gì tôi nghĩ là điểm chính

Điều quan trọng, mục tiêu của tôi với loại điểm chuẩn này không phải là đứng về phía nào trong cái gọi là cuộc chiến ngôn ngữ, mà là để hiểu rõ hơn về hai ngôn ngữ lập trình tuyệt vời và hệ sinh thái gói của chúng

Phần lớn các cuộc thảo luận trong bài viết này dựa trên các tài liệu tham khảo sau (tôi phải nói là khá dài)

Chúng ta nên so sánh Julia với Accelerated-Python như thế nào?

Một điểm khác biệt cơ bản giữa Julia và Python, đó là trong khi mã Julia được đặt cùng nhau trong quá trình biên dịch JIT, thì trong Python, các gói được đặt cùng nhau trong quá trình diễn giải

Sự khác biệt này là rất quan trọng, bởi vì trong khi trình biên dịch Julia có cơ hội thực hiện các tối ưu hóa toàn cầu (i. e. liên thủ tục), hầu hết các dạng Python được tối ưu hóa (như Numba hoặc Cython) sẽ chỉ có cơ hội tối ưu hóa một đoạn mã nhỏ

Nhưng đợi đã. Đó không phải là điều chúng tôi muốn ngay từ đầu sao, chỉ đơn giản là để tối ưu hóa một đoạn mã nhỏ quan trọng về hiệu năng?

Ồ không

Chúng tôi cũng muốn giữ lại những lợi thế của logic cấp cao dễ sử dụng và hệ sinh thái gói phong phú, trong khi chúng tôi gọi đoạn mã được tối ưu hóa này là

Vì vậy, hãy xem liệu chúng ta có giữ được tất cả những thứ đó không, với các hương vị khác nhau của Python được tăng tốc

Mã đơn giản và tối ưu hóa hiệu suất

Điều gì về sự đơn giản của mã?

Mặt khác, Cython yêu cầu lập trình viên viết mã 'keo' đáng kể,

Ngoài ra, Cython ngây thơ sẽ không tối ưu hóa nhiều, bạn phải cho nó biết cụ thể vị trí và những gì cần tối ưu hóa (nó cần được sử dụng một cách khôn ngoan)

Tôi phải thừa nhận rằng tôi thích cách tiếp cận Numba đối với Python nhanh, vì sự thỏa hiệp có vẻ tối thiểu

Chúng tôi vẫn phải lưu ý rằng ngay cả các cấu trúc Python cơ bản như dicts cũng không được hỗ trợ trong Numba, đây chắc chắn có thể là một vấn đề. Tuy nhiên, cách tiếp cận nhắc nhở tôi rất nhiều về việc gọi C hoặc Fortran từ Python, điều mà tôi đã thử một thời gian và điều này hoàn toàn hợp lệ đối với các phân đoạn mã xử lý số trong ứng dụng Python. (Nhưng chúng ta sẽ thấy)

Đối với PyPy đã được chờ đợi từ lâu, có thể thay đổi cuộc chơi nếu nó quản lý để mở rộng quy mô các tính năng được hỗ trợ, hãy xem bài trình bày này 82 của Armin Ronacher (người tạo ra Flask và người đóng góp cho PyPy) về lý do tại sao rất khó để tối ưu hóa Python vì

Julia đấu với Numba. Một điểm chuẩn tối giản

Điểm chuẩn trên toàn cầu so với Mã được tối ưu hóa cho trình biên dịch cục bộ

Cái gọi là “nguyên tắc tổng quát tối thiểu” là một trong những ý tưởng sâu sắc nhất mà tôi đã gặp

Trong bối cảnh hiện tại, điều đó có nghĩa là chúng ta có thể đưa ra trường hợp đơn giản nhất có liên quan để thảo luận về một ý tưởng.

Một mặt, microbenchmark thường quá đơn giản. Ví dụ: tôi đã thấy một số 3 hoặc 4 người so sánh Julia với Numba, điều mà tôi không nghĩ là có liên quan chút nào. Bằng cách đưa ra quyết định dựa trên những tiêu chuẩn vi mô đơn giản đó, chúng tôi đang chấp nhận rủi ro trở nên khôn ngoan và ngu ngốc về đồng đô la

Mặt khác, các ứng dụng thực tế, quy mô lớn có quá nhiều bộ phận chuyển động để được kiểm tra chi tiết

Chúng tôi cần chọn một trường hợp điểm chuẩn đủ thực tế trong đó một số khác biệt cơ bản giữa Python được tối ưu hóa của Julia và Numba thực sự được đưa vào thử nghiệm. Tôi tin rằng điều này đòi hỏi chúng tôi phải nhắm đến việc kiểm tra mức độ kết hợp và hiệu suất của từng ngôn ngữ

Điểm chính xác

Không chần chừ thêm nữa, chúng ta hãy bắt tay vào thực hiện một phép so sánh (hy vọng) có ý nghĩa

Tôi bắt đầu với ý tưởng tạo ra một quy tắc cầu phương bằng một bộ giải phi tuyến tính, nhưng thực ra tôi đã tạo ra một bài kiểm tra đơn giản hơn

Hãy đánh giá hàm sau bằng quy tắc cầu phương

$$ g(p) = \int_{-1}^1 e^{px} dx $$

Chúng tôi thường làm điều này với một tuyến tính đơn giản gọi thư viện cầu phương chuyên dụng. Nhưng để đảm bảo rằng chúng tôi đang kiểm tra cùng một mã chính xác trong mỗi ngôn ngữ, thay vào đó, chúng tôi sẽ sử dụng quy tắc hình thang 7 đường kẻ

Vì vậy, hãy giả sử mã quy tắc hình thang này được cung cấp bởi một chuyên gia và có sẵn thông qua một gói

using LoopVectorization

""" Integrate a function f with the Trapezoidal Rule"""
function quad_trap(f,a,b,N) 
    h = (b-a)/N
    int = h * ( f(a) + f(b) ) / 2
    @turbo for k=1:N-1
        xk = (b-a) * k/N + a
        int = int + h*f(xk)
    end
    return int
end

Và bây giờ đây là một lớp lót mà chúng ta thường viết

g(p) = quad_trap( x -> exp(p*x) - 10, -1, 1, 10000) 
@time g(1)

Chúng ta thường làm nhiều việc hơn là chỉ đánh giá một chức năng tại một điểm duy nhất, nhưng hãy để mọi thứ đơn giản. Hàm g(p) này là khối xây dựng cơ bản mà chúng ta sẽ cần để xây dựng bất kỳ loại logic cấp cao nào xung quanh quy tắc bậc hai được tối ưu hóa

Hãy làm điều này bằng Python thuần túy và Numba ngay bây giờ. Để ngắn gọn, tôi chỉ chia sẻ mã Numba ở đây (có thể lấy mã Python thuần túy bằng cách xóa các tham chiếu đến @nb.njit)

import math
import numba as nb
import time

@nb.njit   
def quad_trap(f,a,b,N):
    h = (b-a)/N
    integral = h * ( f(a) + f(b) ) / 2
    for k in range(N):
        xk = (b-a) * k/N + a
        integral = integral + h*f(xk)
    return integral

Bây giờ, để gọi hàm được tối ưu hóa theo Numba, chúng ta cần cung cấp cho nó một hàm do JIT biên dịch khác, như được mô tả trong tài liệu

Hãy làm theo hai cách khác nhau. một hàm hoàn toàn cố định của x và một hàm được tham số hóa của xp, đây là hàm mà chúng tôi thực sự định sử dụng. Tôi thậm chí đã thêm một số trợ giúp trong trường hợp suy luận kiểu Numba không đúng kiểu

@nb.njit(nb.float64(nb.float64))
def func(x):
    return math.exp(x) - 10

def g(p): 
    @nb.njit(nb.float64(nb.float64))
    def integrand(x):
        return math.exp(p*x) - 10
    return quad_trap(integrand, -1, 1, 10000) 

# warm-up JIT
q1 = quad_trap(func,-1,1,10000)
start_time = time.time()
q1 = quad_trap(func,-1,1,10000)
print("Quadrature--- %s seconds ---" % (time.time() - start_time))

# warm-up JIT
a = f(1)
start_time = time.time()
r = f(1)
print("Eval f(1)--- %s seconds ---" % (time.time() - start_time))

Kết quả

LanguageQuadratureEval g(1)Python thuần túy0. 002760. 00315Numba-Python0. 000270. 11Numba-toán nhanh0. 000150. 11Julia0. 000050. 00005

Tôi tin rằng có một số hiểu biết để đạt được từ so sánh này

Numba nhanh hơn 10 lần so với Python thuần túy đối với điểm chuẩn vi mô của quy tắc cầu phương đơn giản. Tuy nhiên, Julia vẫn nhanh hơn Numba gấp 3 lần, một phần là do tối ưu hóa SIMD được kích hoạt bởi LoopVectorization. jl. Nhưng quan trọng nhất, Numba bị hỏng khi chúng tôi thêm một cấu trúc cấp cao hơn tối thiểu

Lý do của sự đổ vỡ này đã được Jérôme Richard giải thích cho tôi như một câu trả lời cho

Điều bí ẩn đang xảy ra là Numba đang biên dịch lại hàm integrand mỗi khi chúng ta gọi nó. Hơn nữa, những gì chúng tôi đang cố gắng thực hiện ở đây là chuyển một hàm bên trong làm đối số, điều này không được hỗ trợ bởi phiên bản hiện tại của Numba theo

Điều này có nghĩa là Numba không thực sự áp dụng được cho trường hợp sử dụng này (giả sử sử dụng thư viện bậc hai được tối ưu hóa bằng tê liệt và sau đó thêm logic cấp cao hơn lên trên nó)

Để khắc phục, chúng ta có thể tự sửa đổi mã cầu phương (điều này gần như phủ nhận toàn bộ điểm của điểm chuẩn này) hoặc sử dụng Numpy, mà tôi đã điểm chuẩn trước đây trong bài đăng này

Mã của bài kiểm tra đơn giản này có sẵn trong repo Github của tôi. Tôi hoan nghênh các nhận xét trong phần vấn đề của repo hoặc email

Tất nhiên, kết luận này áp dụng cho phiên bản hiện tại của Numba, tại thời điểm viết bài này là phiên bản v0. 50

Khả năng kết hợp, tái sử dụng mã và hệ sinh thái gói

Suy nghĩ cuối cùng của tôi là về khả năng kết hợp, sử dụng lại mã và hệ sinh thái gói

Ngày nay, một hệ sinh thái gói có lẽ là một trong những yếu tố quan trọng nhất (nếu không phải là yếu tố duy nhất) đằng sau sự lựa chọn của chúng ta về ngôn ngữ lập trình này so với ngôn ngữ lập trình khác

Tại thời điểm viết bài này, có khoảng 200. 000 gói Python và chỉ khoảng 6. 000 gói Julia, do đó, điều tự nhiên là hầu hết mọi người sẽ sử dụng Python trong thời điểm hiện tại

Mặc dù có một hệ sinh thái gói Python tuyệt vời, nhưng không có hệ sinh thái gói dành riêng cho Numba nào đang được phát triển

Mặt khác, các gói Julia sắp xếp độc đáo và rất hiệu quả

Vì vậy, tôi đoán là bạn nên mong đợi Python chậm. Tăng tốc nó dường như liên quan đến một sự thỏa hiệp đáng kể, vì chúng ta không thể giữ lại tất cả những điều tốt đẹp về Python. Ở tuổi trưởng thành, ai đó cần viết lại ngày càng nhiều Python thành C để có hiệu suất

Vì vậy, tôi tin rằng, theo thời gian, hệ sinh thái gói Julia sẽ phát triển để mang lại hiệu suất tốt hơn nhiều so với hệ sinh thái Python trong nhiều lĩnh vực. Nó cũng sẽ phát triển nhanh hơn nhiều trên mỗi đơn vị thời gian đầu tư. Đây là lý do tại sao, ở trạng thái hiện tại, Julia đánh giá tôi là sự lựa chọn tốt nhất cho những người phát triển những thứ mới mà không cần quá nhiều đòn bẩy cho các dự án hiện có

Tại sao Julia nhanh hơn Python rất nhiều?

Julia không được thông dịch, do đó tạo nên ngôn ngữ lập trình nhanh , nó cũng được biên dịch đúng lúc hoặc thời gian chạy bằng cách sử dụng khung LLVM. Julia mang đến cho bạn tốc độ tuyệt vời mà không cần bất kỳ kỹ thuật định hình thủ công và tối ưu hóa nào và do đó là giải pháp cho tất cả các vấn đề về hiệu suất của bạn.

Julia có tốt hơn Python cho AI không?

Các nhà khoa học dữ liệu thích Julia hơn Python vì những lợi ích bổ sung mà nó mang lại cho máy học . Julia là một ngôn ngữ lập trình chức năng đa mô hình, chủ yếu được tạo ra cho lập trình thống kê và học máy.

Tại sao nên sử dụng Julia thay vì Python?

Julia thân thiện với toán học hơn, thu hút các nhà khoa học dữ liệu . Vì vậy, họ có thể sử dụng các công thức khoa học của họ như mã. Hơn nữa, sử dụng ít tài nguyên phần cứng hơn để có được các giải pháp điện toán hiệu suất cao. Python là một ngôn ngữ có mục đích chung.

Julia có phải là ngôn ngữ nhanh nhất không?

Julia nhanh . Trên thực tế, mã Julia được tối ưu hóa có thể nhanh như mã C++ hoặc Fortran được tối ưu hóa cao. Hơn nữa, quy trình tối ưu hóa mã này sẽ dễ thực hiện hơn nhiều trong Julia và chương trình kết quả sẽ yêu cầu số dòng mã ít hơn gấp 2 hoặc 3 lần.