مقایسه سلام با پایتون و جولیا برای محاسبات علمی و کار عددی

پایتون با NumPy و اکوسیستمش بر محاسبات علمی حکم می‌راند و جولیا اصلاً برای همین کار طراحی شد. سلام یک کامپایلر بومی و کتابخانه‌ای عددی که تماماً با خود سلام نوشته شده به میدان می‌آورد. این نوشته می‌گوید آن کتابخانه چه می‌کند، ردیف‌های سنجش چه چیزی را می‌سنجند و جای هر زبان کجاست.

در این مقاله
  1. سه رویکرد به محاسبات عددی سریع
  2. حل یک دستگاه معادلات خطی
  3. آمار توصیفی
  4. برازش یک خط
  5. تانسور، مشتق‌گیری خودکار و شبکه‌های عصبی
  6. ردیف‌های ماتریسی سنجش واقعاً چه نشان می‌دهند
  7. سلام، پایتون یا جولیا: کدام را انتخاب کنیم؟
  8. پرسش‌های پرتکرار

برای محاسبات علمی و کار عددی از کدام زبان استفاده کنیم: پایتون، جولیا یا یک زبان کامپایلی؟ پایتون زبان غالب علم و کار با داده است و در عمل سریع است، چون NumPy، SciPy و PyTorch حلقه‌های سنگینشان را در کد کامپایل‌شده‌ی C، C++ و فرترن اجرا می‌کنند. جولیا زبانی است که برای محاسبات عددی طراحی شده، به‌صورت درجا (JIT) از راه LLVM کامپایل می‌شود، توزیع چندگانه (multiple dispatch) دارد و حلقه‌هایش با سرعت کد کامپایل‌شده اجرا می‌شوند. زبان برنامه‌نویسی سلام یک زبان کامپایلی همه‌منظوره است که کتابخانه‌ی استانداردش جبر خطی چگال، آمار، تانسور، مشتق‌گیری خودکار و یادگیری ماشین دارد، همه نوشته‌شده با خود سلام. هر کدام برای نوع متفاوتی از پروژه معنا دارد.

این نوشته کارهای عددی یکسانی را در سه زبان کنار هم می‌گذارد، سه ردیف ماتریس و تانسور سنجش سرعت سلام را با دقت می‌خواند و درباره‌ی فاصله‌ی سلام با اکوسیستم‌های پایتون و جولیا صادق است. برنامه‌های سلام هنگام ساخت این سایت کامپایل و اجرا می‌شوند.

سه رویکرد به محاسبات عددی سریع

خود پایتون در حلقه‌های حسابی کند است. در سنجش سلام روی ۳۹ برنامه‌ای که در همه‌ی زبان‌ها الگوریتم یکسان دارند، پایتون خالص حدود ۵۶ برابر سلام زمان برد. اما هیچ‌کس کار عددی جدی را با پایتون خالص انجام نمی‌دهد: آرایه‌های NumPy عملیات کامل را به کد کامپایل‌شده و کتابخانه‌های بهینه‌ی BLAS و LAPACK می‌سپارند، پس ضرب ماتریس در NumPy با سرعت بومی اجرا می‌شود. هزینه وقتی پیدا می‌شود که کار را نتوان به چند عملیات آرایه‌ای بزرگ تبدیل کرد، یعنی در حلقه‌هایی روی تکه‌های کوچک فراوان، که در آن هر فراخوانی NumPy سربار مفسر را می‌پردازد.

جولیا این دوگانگی را برمی‌دارد. حلقه‌های معمولی جولیا با یک JIT مبتنی بر LLVM به کد ماشین کامپایل می‌شوند و برای گونه‌هایی که با آن‌ها صدا زده می‌شوند تخصیص می‌یابند، پس می‌توانید حلقه را خودتان بنویسید و باز هم سرعت کد کامپایل‌شده را بگیرید. هزینه، تأخیر کامپایل است: نخستین فراخوانی هر تابع برای کامپایل زمان می‌برد، که جولیا از نسخه‌ی ۱٫۹ با ذخیره‌ی کد بومی بسته‌ها آن را بسیار کم کرده است.

سلام مانند C یا Rust پیش از اجرا به یک فایل اجرایی بومی کامپایل می‌شود. حلقه‌ها از نخستین اجرا سریع‌اند، نه مفسری در کار است و نه گرم شدن JIT، و بسته‌های عددی کد معمولی سلام‌اند. هزینه، اکوسیستم است: محاسبات عددی سلام یک کتابخانه‌ی استاندارد است، نه ده‌ها هزار بسته‌ای که پایتون و جولیا دارند.

پایتون (NumPy)جولیاسلام
اجرامفسر؛ عملیات آرایه‌ای در کتابخانه‌های کامپایل‌شدهکامپایل درجا از راه LLVMکد بومی، کامپایل پیش از اجرا
حلقه‌های سریع در کد خودتانفقط از راه NumPy، Numba، Cython و مانند آن‌هابلهبله
جبر خطیNumPy و SciPy روی BLAS/LAPACKLinearAlgebra داخلی روی BLAS/LAPACKبسته‌ی ماتریس، تماماً سلام
یادگیری ماشینPyTorch، JAX، scikit-learnFlux، Lux، MLJai.tensor، ai.autograd، ai.nn، ai.ml
GPUبله، به‌طور گستردهبلهنه
نوت‌بوک و نمودارJupyter، Matplotlib و بسیاری دیگرJupyter، Pluto، Plots، Makieهیچ‌کدام به‌صورت داخلی
اکوسیستمعظیمدر علوم، بزرگیک کتابخانه‌ی استاندارد

حل یک دستگاه معادلات خطی

کار نخست: حل دستگاه ۲ در ۲ زیر، 4x + y = 1 و 2x + 3y = 2. در NumPy و جولیا هر کدام یک خط است:

Python
import numpy as np

A = np.array([[4.0, 1.0], [2.0, 3.0]])
b = np.array([1.0, 2.0])
x = np.linalg.solve(A, b)
print(x, np.linalg.det(A))
Julia
using LinearAlgebra

A = [4.0 1.0; 2.0 3.0]
b = [1.0, 2.0]
x = A \ b
println(x, " ", det(A))

سلام نحوی برای لیترال ماتریس ندارد، پس ماتریس از یک وکتور عدد با ماتریس.از آرایه (matrix.FromArray) ساخته می‌شود. ماتریس.حل (Solve) مثل دو زبان دیگر از تجزیه‌ی LU با محورگیری استفاده می‌کند، و وقتی دستگاه جواب یکتا ندارد به جای متوقف کردن برنامه یک ماتریس خالی برمی‌گرداند:

سلام
واردسازی ماتریس

تابع ریشه:
    ناپایا خانه‌ها := وکتور {} برگردان وکتور<اعشار۶۴>
    هر ع در [۴.۰، ۱.۰، ۲.۰، ۳.۰]:
        خانه‌ها.بیفزا(ع)
    پایان
    ناپایا سمت راست := وکتور {} برگردان وکتور<اعشار۶۴>
    هر ع در [۱.۰، ۲.۰]:
        سمت راست.بیفزا(ع)
    پایان
    ناپایا آ := ماتریس.از آرایه(۲، ۲، خانه‌ها)
    ناپایا ب := ماتریس.از آرایه(۲، ۱، سمت راست)
    ناپایا ایکس := ماتریس.حل(آ، ب)
    اگر ایکس.تهی است():
        سرچاپ "جواب یکتا ندارد"
        برگشت ۱
    پایان
    سرچاپ "x ="، ماتریس.به رشته با دقت(ماتریس.ترانهاده(ایکس)، ۴)
    سرچاپ "det ="، ماتریس.دترمینان(آ)
    آ.آزادسازی()
    ب.آزادسازی()
    ایکس.آزادسازی()
پایان
خروجیx = 0.1000 0.6000 det = 10

نسخه‌ی سلام آشکارا بلندتر است. ساختن ماتریس‌ها، بررسی نتیجه‌ی خالی و آزاد کردن تک‌تک آن‌ها با آزادسازی() کارهایی است که پایتون و جولیا با لیترال، استثنا و زباله‌روب به جای شما انجام می‌دهند. این الگوی کلی این مقایسه است: محاسبات عددی سلام توانمند است، اما زبان نه مثل جولیا پیرامون آرایه‌ها طراحی شده و نه مثل پایتون در طول دو دهه با آن‌ها سازگار شده است.

بسته‌ی ماتریس (matrix) خیلی فراتر از این می‌رود: تجزیه‌های LU، QR، Cholesky و SVD، مقدارهای ویژه‌ی ماتریس‌های متقارن و عمومی، رتبه، شبه‌وارون، فضای پوچ، نمایی و ریشه‌ی دوم ماتریس، حل‌کننده‌های تکراری (گرادیان مزدوج، ژاکوبی، تکرار توانی)، نُرم‌ها، کوواریانس، همبستگی و PCA. همه روی عددهای ممیز شناور ۶۴ بیتی و در یک گونه‌ی ماتریس سطرمحور کار می‌کنند.

آمار توصیفی

برای آماره‌های خلاصه، پایتون ماژول statistics و NumPy را دارد و جولیا کتابخانه‌ی استاندارد Statistics را:

Python
import numpy as np

temps = np.array([21.5, 23.0, 19.8, 25.1, 22.4, 30.2, 20.9])
print(temps.mean(), np.median(temps), temps.std(ddof=1), np.percentile(temps, 90))
Julia
using Statistics

temps = [21.5, 23.0, 19.8, 25.1, 22.4, 30.2, 20.9]
println(mean(temps), " ", median(temps), " ", std(temps), " ", quantile(temps, 0.9))

بسته‌ی آمار سلام روی یک وکتور<اعشار۶۴> کار می‌کند. مانند std در جولیا و NumPy با ddof=1، تابع انحراف معیار (Stdev) انحراف معیار نمونه را حساب می‌کند، و صدک (Percentile) میان دو مقدار نزدیک‌تر درون‌یابی خطی می‌کند، که رفتار پیش‌فرض NumPy و جولیا هم همین است:

سلام
واردسازی تبدیل
واردسازی آمار

تابع نمایش(برچسب: رشته، مقدار عددی: اعشار۶۴):
    سرچاپ برچسب، تبدیل.قالب اعشار با دقت(مقدار عددی، ۲)
پایان

تابع ریشه:
    ناپایا دماها := وکتور {} برگردان وکتور<اعشار۶۴>
    دیرکن دماها.آزادکن()
    هر د در [۲۱.۵، ۲۳.۰، ۱۹.۸، ۲۵.۱، ۲۲.۴، ۳۰.۲، ۲۰.۹]:
        دماها.بیفزا(د)
    پایان
    نمایش("میانگین:"، آمار.میانگین(دماها))
    نمایش("میانه:"، آمار.میانه(دماها))
    نمایش("انحراف معیار:"، آمار.انحراف معیار(دماها))
    نمایش("صدک ۹۰:"، آمار.صدک(دماها، ۹۰.۰))
پایان
خروجیمیانگین: 23.27 میانه: 22.40 انحراف معیار: 3.49 صدک ۹۰: 27.14

عددهای محاسبه‌شده با رقم‌های لاتین چاپ می‌شوند، اما رقم‌های فارسی درون یک رشته (مثل «صدک ۹۰») همان‌طور که نوشته شده‌اند می‌مانند. بسته کوچک است: جمع، میانگین، میانه، کمینه، بیشینه، واریانس و انحراف معیار نمونه و جامعه، میانگین هندسی و صدک‌ها. یک نکته در عمل مهم است: وکتور خالی به جای خطا صفر برمی‌گرداند، پس اول طول را بررسی کنید. معادلی برای آزمون‌های آماری و توزیع‌های SciPy وجود ندارد، و دیتافریمی مانند pandas یا DataFrames.jl هم نیست.

برازش یک خط

رگرسیون کمترین مربعات همان الگو را نشان می‌دهد. NumPy یک چندجمله‌ای را با یک فراخوانی برازش می‌کند، و جولیا دستگاه فرامعین را با همان عملگر بک‌اسلشی حل می‌کند که برای دستگاه‌های مربعی به کار می‌برد:

Python
import numpy as np

hours = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
score = np.array([52.0, 57.0, 66.0, 70.0, 77.0])
slope, intercept = np.polyfit(hours, score, 1)
print(intercept, slope)
Julia
hours = [1.0, 2.0, 3.0, 4.0, 5.0]
score = [52.0, 57.0, 66.0, 70.0, 77.0]
X = [ones(length(hours)) hours]
intercept, slope = X \ score
println(intercept, " ", slope)

ماتریس.رگرسیون خطی (matrix.LinearRegression) در سلام ستون عرض از مبدأ را اضافه می‌کند و مسئله‌ی کمترین مربعات را حل می‌کند:

سلام
واردسازی ماتریس

تابع ستون(مقادیر: اعشار۶۴[]): ماتریس.ماتریس:
    ناپایا داده := وکتور {} برگردان وکتور<اعشار۶۴>
    هر م در مقادیر:
        داده.بیفزا(م)
    پایان
    برگشت ماتریس.از آرایه(مقادیر.طول()، ۱، داده)
پایان

تابع ریشه:
    ناپایا ساعت‌ها := ستون([۱.۰، ۲.۰، ۳.۰، ۴.۰، ۵.۰])
    ناپایا نمره‌ها := ستون([۵۲.۰، ۵۷.۰، ۶۶.۰، ۷۰.۰، ۷۷.۰])
    ناپایا برازش := ماتریس.رگرسیون خطی(ساعت‌ها، نمره‌ها)
    سرچاپ "نمره ="، ماتریس.به رشته با دقت(ماتریس.پرشده(۱، ۱، برازش.عنصر(۰، ۰))، ۲)، "+"، ماتریس.به رشته با دقت(ماتریس.پرشده(۱، ۱، برازش.عنصر(۱، ۰))، ۲)، "* ساعت"
    ساعت‌ها.آزادسازی()
    نمره‌ها.آزادسازی()
    برازش.آزادسازی()
پایان
خروجینمره = 45.50 + 6.30 * ساعت

برای چیزی بیش از یک خط، بسته‌ی ai.ml از شکل scikit-learn پیروی می‌کند: یک ساختار برای هر برآوردگر با fit، predict و score، که رگرسیون خطی و منظم‌شده، رگرسیون لجستیک، k نزدیک‌ترین همسایه، بیز ساده، درخت تصمیم، جنگل تصادفی، تقویت گرادیان، ماشین بردار پشتیبان، k-means، DBSCAN، مدل آمیخته‌ی گاوسی، PCA، t-SNE و اعتبارسنجی متقابل را پوشش می‌دهد.

تانسور، مشتق‌گیری خودکار و شبکه‌های عصبی

فراتر از جبر خطی ۶۴ بیتی، سلام یک بسته‌ی تانسور اعشار۳۲ (f32) دارد (ai.tensor) با پخش (broadcasting)، کاهش‌ها، کانولوشن و ضرب ماتریسی که برای ورودی‌های بزرگ از چند نخ استفاده می‌کند؛ یک بسته‌ی مشتق‌گیری خودکار حالت معکوس مبتنی بر نوار (ai.autograd)؛ لایه‌های شبکه‌ی عصبی تا LSTM، GRU، توجه چندسر و بلوک‌های ترنسفورمر (ai.nn)؛ بهینه‌سازهایی مانند SGD، Adam و AdamW؛ بارگذارهایی برای مدل‌های ONNX و وزن‌های GGUF؛ و خواننده و نویسنده‌ی فایل‌های .npy و .npz در NumPy، که اگر داده را در پایتون آماده می‌کنید پل عملی میان دو طرف است. همه‌ی این‌ها تماماً سلام، با بذر (seed) و قطعی‌اند: یک بذر یکسان روی هر ماشینی نتیجه‌ی یکسان می‌دهد.

این‌ها فقط روی پردازنده (CPU) اجرا می‌شوند. پشتیبانی GPU، دقت آمیخته و چیزی در مقیاس PyTorch یا JAX وجود ندارد، پس آموزش مدل‌های بزرگ خارج از دامنه است. نوشته‌ی محاسبات علمی و یادگیری ماشین در سلام این بسته‌ها را در عمل نشان می‌دهد.

ردیف‌های ماتریسی سنجش واقعاً چه نشان می‌دهند

صفحه‌ی سرعت سلام ۴۲ برنامه را فهرست می‌کند. سه تا از آن‌ها، ردیف‌های ۴۰ تا ۴۲، کتابخانه‌ی ماتریس و تانسور خود سلام را صدا می‌زنند، در حالی که نسخه‌های C، C++، Rust و Go به شکل حلقه‌های ساده نوشته شده‌اند و نسخه‌های پایتون از NumPy استفاده می‌کنند. صفحه صریح می‌گوید این ردیف‌ها کتابخانه‌ها را مقایسه می‌کنند، نه کامپایلرها را، و آن‌ها را از میانگین‌هایش بیرون می‌گذارد. این عددها به میلی‌ثانیه و سریع‌ترینِ ۱۰ اجراست:

برنامهCC++RustGoسلامپایتون (NumPy)
۴۰، ضرب ماتریس f64 با ابعاد ۴۰۰ در ۴۰۰ (matrix.MatMul)۴۲٫۵۴۳٫۸۶۴٫۳۴۷٫۲۱۴٫۳۱۲۱٫۸
۴۱، ۲۰۰٬۰۰۰ عملیات کوچک ۴ در ۴ (بسته‌ی matrix)۴٫۱۵٫۰۵٫۲۱۵٫۹۳۶٫۴۱٬۱۷۴٫۴
۴۲، ضرب تانسور f32 با ابعاد ۴۰۰ در ۴۰۰ (tensor.MatMul)۴۰٫۳۳۸٫۱۵۵٫۷۴۳٫۰۷٫۳۱۱۹٫۱

چطور بخوانیمشان:

  • ردیف‌های ۴۰ و ۴۲ به سود سلام‌اند، چون یک ضرب کتابخانه‌ای با یک حلقه‌ی سه‌تایی ساده مقایسه می‌شود. matrix.MatMul در سلام در بلوک‌هایی به اندازه‌ی حافظه‌ی نهان کار می‌کند و ضرب تانسوری در این اندازه روی چند نخ اجرا می‌شود. اگر برنامه‌ی C یک BLAS بهینه را صدا می‌زد، تصویر بسیار متفاوتی به دست می‌آمد. این ردیف‌ها می‌گویند کتابخانه‌ی سلام به‌خوبی مهندسی شده، نه اینکه سلام حلقه‌ها را بهتر از C کامپایل می‌کند.
  • ردیف ۴۱ برعکس است. دویست هزار ضرب کوچک یعنی صدها هزار تخصیص هیپ و فراخوانی کتابخانه‌ی همه‌منظوره، در حالی که نسخه‌ی C دو آرایه‌ی ۴ در ۴ را روی پشته نگه می‌دارد و gcc بیشتر کار حلقه را حذف می‌کند. سلام ۳۶٫۴ میلی‌ثانیه گرفت در برابر ۴٫۱ میلی‌ثانیه‌ی C. برای تعداد زیادی ماتریس کوچک با اندازه‌ی ثابت، آرایه‌های ساده در سلام انتخاب بهتری‌اند.
  • پایتون با NumPy در ردیف‌های ۴۰ و ۴۲ (حدود ۱۲۰ میلی‌ثانیه) خیلی بیشتر از فاصله‌ی معمول ۵۶ برابری‌اش به زبان‌های کامپایلی نزدیک است، چون NumPy ضرب را در کد کامپایل‌شده انجام می‌دهد. ردیف ۴۱ حالت مخالف را نشان می‌دهد: سربار هر فراخوانی روی آرایه‌های کوچک فراوان، ۱٬۱۷۴ میلی‌ثانیه.

جولیا جزو این سنجش نیست، پس مقایسه‌ی اندازه‌گیری‌شده‌ای با آن وجود ندارد. همه‌ی برنامه‌های این مجموعه کوتاه‌اند، راه‌اندازی فرایند جزو هر زمان است و اجراها روی یک ماشین مجازی اشتراکی CI انجام شده‌اند، پس فاصله‌های کوچک معنایی ندارند. نوشته‌ی سنجش روش کار را شرح می‌دهد.

سلام، پایتون یا جولیا: کدام را انتخاب کنیم؟

پایتون را انتخاب کنید وقتی:

  • به اکوسیستم نیاز دارید: SciPy، pandas، scikit-learn، PyTorch، JAX، Jupyter و رسم نمودار؛
  • داده را به‌صورت تعاملی کاوش می‌کنید و سریع تکرار می‌کنید؛
  • کارتان عملیات آرایه‌ای بزرگ یا آموزش روی GPU است، جایی که کتابخانه‌ها بار سنگین را می‌کشند.

جولیا را انتخاب کنید وقتی:

  • الگوریتم‌های عددی تازه‌ای می‌نویسید که حلقه‌های درونی‌شان در قالب عملیات آرایه‌ای نمی‌گنجد؛
  • نحو ریاضی، توزیع چندگانه و یک اکوسیستم علمی قوی (معادلات دیفرانسیل، بهینه‌سازی، برنامه‌نویسی احتمالاتی) می‌خواهید؛
  • محاسبات طولانی‌مدت تأخیر JIT را بی‌اهمیت می‌کند.

سلام را انتخاب کنید وقتی:

  • محاسبات عددی بخشی از یک برنامه‌ی بومی بزرگ‌تر است، مثل یک سرور، یک ابزار خط فرمان یا یک سیستم نهفته، و می‌خواهید در همان زبان و همان فایل اجرایی باشد؛
  • یک فایل اجرایی خودبسنده بدون مفسر، محیط اجرا یا محیط بسته‌ای برای نصب لازم دارید؛
  • نتیجه‌های قطعی روی پردازنده، مدل‌های کوچک و یادگیری ماشین کلاسیک برایتان کافی است؛
  • روش‌های عددی را به فارسی یا انگلیسی درس می‌دهید و می‌خواهید دانشجوها بتوانند تک‌تک خط‌های کتابخانه را بخوانند.

پرسش‌های پرتکرار

آیا زبان سلام می‌تواند جای NumPy را بگیرد؟

برای جبر خطی چگال در اندازه‌های متوسط و آمار درون یک برنامه‌ی کامپایل‌شده، بسته‌های ماتریس، آمار و ai.tensor بخش زیادی از همان زمینه را پوشش می‌دهند. اما پشتیبانی GPU، پشتوانه‌ی BLAS و اکوسیستمی هم‌پای SciPy و pandas ندارند، پس برای کار علم داده NumPy ابزار بهتری می‌ماند.

آیا سلام از جولیا سریع‌تر است؟

اندازه‌گیری منتشرشده‌ای این دو را مقایسه نکرده است. هر دو برای حلقه‌های داغ از راه LLVM کد بومی تولید می‌کنند؛ جولیا در زمان اجرا و سلام پیش از اجرا. پس سلام از تأخیر JIT دوری می‌کند، در حالی که تخصیص گونه‌ای جولیا و جبر خطی مبتنی بر BLAS آن پخته و سریع است.

آیا سلام برای کاوش داده REPL یا نوت‌بوک دارد؟

سلام یک محیط تعاملی (salam cli) و یک ویرایشگر آنلاین دارد، اما محیط نوت‌بوکی مانند Jupyter یا Pluto و کتابخانه‌ی رسم نمودار ندارد.

آیا سلام می‌تواند داده‌ای را که در پایتون آماده شده بخواند؟

بله. بسته‌ی npy فایل‌های .npy و .npz در NumPy را می‌خواند و می‌نویسد، و بسته‌ی داده (data) جدول‌های CSV را در ماتریس بارگذاری می‌کند، پس داده می‌تواند میان یک روند کار پایتونی و یک برنامه‌ی سلام جابه‌جا شود.

سلام و زبان‌های دیگر

آیا یک زبان برنامه‌نویسی تازه می‌تواند با زبان‌های چنددهه‌ای رقابت کند؟ جایگاه سلام در اکوسیستم جهانی

هر زبان محبوبی روزی تازه بوده و بیشتر زبان‌های تازه هرگز محبوب نمی‌شوند. این نوشته بررسی می‌کند چه چیزی Go، Rust، Kotlin و دیگران را به پذیرش رساند، و سلام را با همان نیروها می‌سنجد، بی‌آنکه وانمود کند فاصله کم است.

خواندن مقاله
سلام و زبان‌های دیگر

مقایسه‌ی سلام و Rust: سرعت، مدیریت حافظه، ایمنی و کنترل سطح پایین

Rust ایمنی حافظه را در زمان کامپایل اثبات می‌کند و سلام نه. این مقایسه نشان می‌دهد هر زبان چه چیزی را بررسی می‌کند، برنامه‌های یکسان را به هر دو زبان می‌آورد، سرعت اندازه‌گیری‌شده را می‌گوید و جای مناسب هر کدام را نشان می‌دهد.

خواندن مقاله