مقدمة: لماذا تحتاج إلى تحليل أداء كود Rust؟
تشتهر Rust بسرعتها وأمانها، ولكن حتى أسرع كود يمكن أن يعمل ببطء بسبب اختناقات غير واضحة. تحليل الأداء هو عملية قياس وقت التنفيذ، واستخدام الذاكرة، والموارد الأخرى، مما يساعد في العثور على "الاختناقات". بدون تحليل الأداء، تتحول عملية التحسين إلى تخمين: قد "تحسن" أجزاء من الكود لا تؤثر على الأداء على الإطلاق.
في هذه المقالة، سنستعرض أدوات تحليل الأداء الرئيسية لـ Rust: perf، flamegraph، cargo-flamegraph، والقياس المدمج. ستتعلم كيفية العثور على الدوال البطيئة، وعمليات تخصيص الذاكرة، وأخطاء التخزين المؤقت، وكذلك كيفية تفسير البيانات التي تم الحصول عليها.
أدوات تحليل أداء Rust: اختيار النهج الصحيح
1. القياس المدمج مع #[bench]
الخطوة الأولى نحو تحليل الأداء هي كتابة معايير القياس. في Rust، يوجد دعم مدمج لمعايير القياس (غير مستقر، يتطلب nightly). مثال:
// في ملف benches/my_bench.rs#![feature(test)]extern crate test;
use test::Bencher;
#[bench]fn bench_my_function(b: &mut Bencher) { let data = vec![1, 2, 3, 4, 5]; b.iter(|| { // الدالة المختبرة let _ = data.iter().sum::<i32>(); });}التشغيل: cargo bench. ستحصل على قياسات دقيقة للوقت، لكن هذا لن يظهر أين يتم استهلاك الوقت بالضبط داخل الدالة.
2. perf — محلل الأداء على مستوى النواة
أداة perf (لنظام Linux) تجمع عينات من عدادات المعالج. تُظهر أي أسطر من الكود يتم تنفيذها بشكل متكرر.
التثبيت: sudo apt install linux-tools-common linux-tools-generic.
مثال على الاستخدام:
# نترجم مع رموز التصحيحcargo build --release
# نبدأ تحليل الأداءsudo perf record ./target/release/my_program
# نعرض التقريرsudo perf reportفي التقرير، سترى الدوال مرتبة حسب النسبة المئوية لوقت التنفيذ. هذه هي المرشحة الأولى للتحسين.
3. flamegraph — تصور مكدس الاستدعاءات
Flamegraph هو رسم بياني SVG، حيث يتوافق عرض كل مستطيل مع وقت تنفيذ الدالة. كلما كان المستطيل أعرض، زاد الوقت المستهلك في هذه الدالة أو استدعاءاتها الفرعية.
تثبيت cargo-flamegraph:
cargo install flamegraphالتشغيل:
cargo flamegraph --bin my_programالنتيجة — ملف flamegraph.svg. افتحه في المتصفح. ابحث عن "الهضاب" العريضة — هذه هي الاختناقات.
مثال عملي: العثور على اختناق في كود Rust
لننظر في برنامج بسيط يعالج مصفوفة كبيرة من البيانات:
fn process(data: &[u8]) -> Vec<u8> { let mut result = Vec::with_capacity(data.len()); for &byte in data { result.push(byte.wrapping_mul(2)); } result}
fn main() { let data = vec![0u8; 10_000_000]; let _ = process(&data);}نقوم بالتجميع وبدء تحليل الأداء:
cargo build --releasecargo flamegraph --bin my_programعند فتح flamegraph، نرى أن الوقت الرئيسي يُستهلك في الحلقة for واستدعاء wrapping_mul. نلاحظ أن Vec::push قد يسبب إعادة تخصيص إذا لم تكن السعة محسوبة بدقة. في حالتنا، تم تعيين with_capacity بشكل صحيح، لكن الحلقة نفسها هي الاختناق.
التحسين: استخدام المكررات و map:
fn process_optimized(data: &[u8]) -> Vec<u8> { data.iter().map(|&b| b.wrapping_mul(2)).collect()}يظهر القياس المتكرر تسارعًا بنسبة 10-15% بفضل التوجيه (التوجيه التلقائي من LLVM).