دليل أداء Rust لمطوري Hayabusa¶
المؤلف¶
Fukusuke Takahashi
الترجمة الإنجليزية¶
Zach Mathis (@yamatosecurity)
حول هذا المستند¶
Hayabusa (بالإنجليزية: "صقر الشاهين") هي أداة سريعة للتحليل الجنائي طورتها مجموعة Yamato Security في اليابان. وهي مطورة بلغة Rust من أجل (تتبع) التهديدات بسرعة صقر الشاهين. إن Rust لغة سريعة في حد ذاتها، ومع ذلك هناك العديد من المزالق التي يمكن أن تؤدي إلى بطء السرعة وارتفاع استخدام الذاكرة. لقد أنشأنا هذا المستند بناءً على تحسينات أداء فعلية في Hayabusa (انظر سجل التغييرات هنا)، لكن هذه التقنيات ينبغي أن تكون قابلة للتطبيق على برامج Rust الأخرى أيضًا. نأمل أن تستفيد من المعرفة التي اكتسبناها من خلال تجربتنا وأخطائنا.
تحسين السرعة¶
تغيير مُخصِّص الذاكرة¶
قد يؤدي مجرد تغيير مُخصِّص الذاكرة الافتراضي إلى تحسين السرعة بشكل كبير. على سبيل المثال، وفقًا لهذه القياسات المرجعية، فإن مُخصِّصَيْ الذاكرة التاليَيْن
أسرع بكثير من مُخصِّص الذاكرة الافتراضي. لقد تمكنا من تأكيد تحسن كبير في السرعة عند تغيير مُخصِّص الذاكرة لدينا من jemalloc إلى mimalloc، لذا جعلنا mimalloc الخيار الافتراضي منذ الإصدار 1.8.0. (مع أن mimalloc يستخدم ذاكرة أكثر قليلًا من jemalloc.)
قبل ¶
بعد ¶
كل ما تحتاج إليه هو تنفيذ الخطوتين التاليتين لتغيير مُخصِّص الذاكرة العام:
- أضف حزمة mimalloc إلى قسم [dependencies] في ملف
Cargo.toml: - عرّف أنك تريد استخدام mimalloc تحت #[global_allocator] في مكان ما من البرنامج: هذا كل ما تحتاج إلى فعله لتغيير مُخصِّص الذاكرة.
الفعالية(مثال واقعي من طلب سحب) ¶
يعتمد مدى تحسن السرعة على البرنامج، لكن في المثال التالي
أدى تغيير مُخصِّص الذاكرة إلى mimalloc إلى زيادة في الأداء بنسبة 20-30٪ على معالجات Intel. (لسبب ما، لم تكن هناك زيادة في الأداء بنفس القدر على أجهزة macOS المعتمدة على ARM.)
تقليل معالجة الإدخال/الإخراج داخل الحلقات¶
معالجة الإدخال/الإخراج للقرص أبطأ بكثير من المعالجة في الذاكرة. لذلك، من المستحسن تجنب معالجة الإدخال/الإخراج قدر الإمكان، خاصة داخل الحلقات.
قبل ¶
يوضح المثال أدناه فتح ملف يحدث مليون مرة داخل حلقة:
use std::fs;
fn main() {
for _ in 0..1000000 {
let f = fs::read_to_string("sample.txt").unwrap();
f.len();
}
}
بعد ¶
بفتح الملف خارج الحلقة كما يلي
use std::fs;
fn main() {
let f = fs::read_to_string("sample.txt").unwrap();
for _ in 0..1000000 {
f.len();
}
}
الفعالية(مثال واقعي من طلب سحب) ¶
في المثال التالي، أمكن تنفيذ معالجة الإدخال/الإخراج عند التعامل مع نتيجة كشف واحدة في كل مرة خارج الحلقة:
أدى هذا إلى تحسن في السرعة بحوالي 20٪.
تجنّب تجميع التعبيرات النمطية داخل الحلقات¶
يُعد تجميع التعبيرات النمطية عملية مكلفة جدًا مقارنة بمطابقة التعبيرات النمطية. لذلك، يُنصح بتجنب تجميع التعبيرات النمطية قدر الإمكان، خاصة داخل الحلقات.
قبل ¶
على سبيل المثال، تنشئ العملية التالية 100,000 محاولة لمطابقة تعبير نمطي داخل حلقة:
extern crate regex;
use regex::Regex;
fn main() {
let text = "1234567890";
let match_str = "abc";
for _ in 0..100000 {
if Regex::new(match_str).unwrap().is_match(text){ // Regular expression compilation in a loop
println!("matched!");
}
}
}
بعد ¶
بإجراء تجميع التعبير النمطي خارج الحلقة، كما هو موضح أدناه
extern crate regex;
use regex::Regex;
fn main() {
let text = "1234567890";
let match_str = "abc";
let r = Regex::new(match_str).unwrap(); // Compile the regular expression outside the loop
for _ in 0..100000 {
if r.is_match(text) {
println!("matched!");
}
}
}
الفعالية(مثال واقعي من طلب سحب) ¶
في المثال التالي، يُجرى تجميع التعبير النمطي خارج الحلقة ويُخزَّن مؤقتًا.
أدى هذا إلى تحسينات كبيرة في السرعة.
استخدام الإدخال/الإخراج المُخزَّن مؤقتًا (buffer IO)¶
بدون الإدخال/الإخراج المُخزَّن مؤقتًا، يكون إدخال/إخراج الملفات بطيئًا. مع الإدخال/الإخراج المُخزَّن مؤقتًا، تُجرى عمليات الإدخال/الإخراج عبر مخازن مؤقتة في الذاكرة، مما يقلل عدد استدعاءات النظام ويحسن السرعة.
قبل ¶
على سبيل المثال، في العملية التالية، تحدث write مليون مرة.
use std::fs::File;
use std::io::{BufWriter, Write};
fn main() {
let mut f = File::create("sample.txt").unwrap();
for _ in 0..1000000 {
f.write(b"hello world!");
}
}
بعد ¶
باستخدام BufWriter كما يلي
use std::fs::File;
use std::io::{BufWriter, Write};
fn main() {
let mut f = File::create("sample.txt").unwrap();
let mut writer = BufWriter::new(f);
for _ in 0..1000000 {
writer.write(b"some text");
}
writer.flush().unwrap();
}
الفعالية(مثال واقعي من طلب سحب) ¶
الطريقة الموضحة أعلاه نُفِّذت هنا
وأدت إلى تحسينات كبيرة في السرعة في معالجة الإخراج.
استخدام طرق String القياسية بدلًا من التعبيرات النمطية¶
بينما يمكن للتعبيرات النمطية تغطية أنماط مطابقة معقدة، فإنها أبطأ من طرق String القياسية. لذلك، يكون استخدام طرق String القياسية أسرع لمطابقة السلاسل النصية البسيطة مثل التالي.
- مطابقة البداية(التعبير النمطي:
foo.*)-> String::starts_with() - مطابقة النهاية(التعبير النمطي:
.*foo)-> String::ends_with() - مطابقة الاحتواء(التعبير النمطي:
.*foo.*)-> String::contains()
قبل ¶
على سبيل المثال، يُجري الكود التالي مطابقة النهاية بتعبير نمطي مليون مرة.
extern crate regex;
use regex::Regex;
fn main() {
let text = "1234567890";
let match_str = ".*abc";
let r = Regex::new(match_str).unwrap();
for _ in 0..1000000 {
if r.is_match(text) {
println!("matched!");
}
}
}
بعد ¶
باستخدام String::ends_with() كما يلي
fn main() {
let text = "1234567890";
let match_str = "abc";
for _ in 0..1000000 {
if text.ends_with(match_str) {
println!("matched!");
}
}
}
الفعالية(مثال واقعي من طلب سحب) ¶
بما أن Hayabusa تتطلب مقارنة سلاسل نصية غير حساسة لحالة الأحرف، فإننا نستخدم to_lowercase() ثم نطبق الطريقة أعلاه. حتى مع ذلك، في الأمثلة التالية
- Imporving speed by changing wildcard search process from regular expression match to starts_with/ends_with match #890
- Improving speed by using eq_ignore_ascii_case() before regular expression match #884
تحسنت السرعة بحوالي 15٪ مقارنة بما كانت عليه سابقًا.
التصفية حسب طول السلسلة النصية¶
اعتمادًا على خصائص السلاسل النصية التي يجري التعامل معها، قد تؤدي إضافة مرشِّح بسيط إلى تقليل عدد محاولات مطابقة السلاسل النصية وتسريع العملية. إذا كنت غالبًا ما تقارن سلاسل نصية ذات أطوال غير ثابتة وغير متطابقة، فيمكنك تسريع العملية باستخدام طول السلسلة النصية كمرشِّح أولي.
قبل ¶
على سبيل المثال، يحاول الكود التالي مليون مطابقة لتعبير نمطي.
extern crate regex;
use regex::Regex;
fn main() {
let text = "1234567890";
let match_str = "abc";
let r = Regex::new(match_str).unwrap();
for _ in 0..1000000 {
if r.is_match(text) {
println!("matched!");
}
}
}
بعد ¶
باستخدام String::len() كمرشِّح أولي، كما هو موضح أدناه
extern crate regex;
use regex::Regex;
fn main() {
let text = "1234567890";
let match_str = "abc";
let r = Regex::new(match_str).unwrap();
for _ in 0..1000000 {
if text.len() == match_str.len() { // Primary filter by string length
if r.is_match(text) {
println!("matched!");
}
}
}
}
الفعالية(مثال واقعي من طلب سحب) ¶
في المثال التالي، تُستخدم الطريقة أعلاه.
أدى هذا إلى تحسين السرعة بحوالي 15٪.
لا تُجرِ التجميع باستخدام codegen-units=1¶
تنصح العديد من المقالات حول تحسين الأداء مع Rust بإضافة codegen-units = 1 تحت قسم [profile.release].
سيؤدي هذا إلى أوقات تجميع أبطأ لأن الافتراضي هو التجميع بالتوازي، لكنه نظريًا ينبغي أن يؤدي إلى كود أكثر تحسينًا وأسرع.
ومع ذلك، في اختباراتنا، تعمل Hayabusa في الواقع بشكل أبطأ مع تشغيل هذا الخيار، ويستغرق التجميع وقتًا أطول، لذا نُبقيه معطلًا.
حجم البرنامج التنفيذي الثنائي أصغر بحوالي 100 كيلوبايت، لذا قد يكون هذا مثاليًا للأنظمة المضمَّنة حيث تكون مساحة القرص الصلب محدودة.
تقليل استخدام الذاكرة¶
تجنّب الاستخدام غير الضروري لـ clone() و to_string() و to_owned()¶
يُعد استخدام clone() أو to_string() طريقة سهلة لحل أخطاء التجميع المتعلقة بـالملكية. ومع ذلك، فإنها عادة ما تؤدي إلى استخدام مرتفع للذاكرة وينبغي تجنبها. من الأفضل دائمًا أن ترى أولًا ما إذا كان بإمكانك استبدالها بـمراجع منخفضة التكلفة.
قبل ¶
على سبيل المثال، إذا كنت تريد التكرار على نفس Vec عدة مرات، فيمكنك استخدام clone() للتخلص من أخطاء التجميع.
fn main() {
let lst = vec![1, 2, 3];
for x in lst.clone() { // In order to eliminate compile errors
println!("{x}");
}
for x in lst {
println!("{x}");
}
}
بعد ¶
ومع ذلك، باستخدام المراجع كما هو موضح أدناه، يمكنك إزالة الحاجة إلى استخدام clone().
fn main() {
let lst = vec![1, 2, 3];
for x in &lst { // Eliminate compile errors with a reference
println!("{x}");
}
for x in lst {
println!("{x}");
}
}
الفعالية(مثال واقعي من طلب سحب) ¶
في المثال التالي، باستبدال الاستخدام غير الضروري لـclone() وto_string() وto_owned()،
تمكنا من تقليل استخدام الذاكرة بشكل كبير.
استخدام Iterator بدلًا من Vec¶
يحتفظ Vec بجميع العناصر في الذاكرة، لذا فإنه يستخدم قدرًا كبيرًا من الذاكرة بما يتناسب مع عدد العناصر. إذا كانت معالجة عنصر واحد في كل مرة كافية، فإن استخدام Iterator بدلًا من ذلك سيستخدم ذاكرة أقل بكثير.
قبل ¶
على سبيل المثال، تقرأ الدالة return_lines() التالية ملفًا بحجم حوالي 1 جيجابايت وتُرجع Vec:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn return_lines() -> Vec<String> {
let f = File::open("sample.txt").unwrap();
let buf = BufReader::new(f);
buf.lines()
.map(|l| l.expect("Could not parse line"))
.collect()
}
fn main() {
let lines = return_lines();
for line in lines {
println!("{}", line)
}
}
بعد ¶
بدلًا من ذلك ينبغي أن تُرجع سمة Iterator كما يلي:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn return_lines() -> impl Iterator<Item=String> {
let f = File::open("sample.txt").unwrap();
let buf = BufReader::new(f);
buf.lines()
.map(|l| l.expect("Could not parse line"))
// ここでcollect()せずに、Iteratorを戻り値として返す
}
fn main() {
let lines = return_lines();
for line in lines {
println!("{}", line)
}
}
Box<dyn Iterator<Item = T>> كما يلي:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn return_lines(need_filter:bool) -> Box<dyn Iterator<Item = String>> {
let f = File::open("sample.txt").unwrap();
let buf = BufReader::new(f);
if need_filter {
let result= buf.lines()
.filter_map(|l| l.ok())
.map(|l| l.replace("A", "B"));
return Box::new(result)
}
let result= buf.lines()
.map(|l| l.expect("Could not parse line"));
Box::new(result)
}
fn main() {
let lines = return_lines(true);
for line in lines {
println!("{}", line)
}
}
الفعالية(مثال واقعي من طلب سحب) ¶
يستخدم المثال التالي الطريقة الموضحة أعلاه:
عند الاختبار على ملف JSON بحجم 1.7 جيجابايت، انخفضت الذاكرة بنسبة 75٪.
استخدام حزمة compact_str عند التعامل مع السلاسل النصية القصيرة¶
عند التعامل مع عدد كبير من السلاسل النصية القصيرة الأقل من 24 بايت، يمكن استخدام حزمة compact_str لتقليل استخدام الذاكرة.
قبل ¶
في المثال أدناه، يحتفظ Vec بعشرة ملايين سلسلة نصية.
fn main() {
let v: Vec<String> = vec![String::from("ABCDEFGHIJKLMNOPQRSTUV"); 10000000];
// do some kind of processing
}
بعد ¶
من الأفضل استبدالها بـCompactString:
use compact_str::CompactString;
fn main() {
let v: Vec<CompactString> = vec![CompactString::from("ABCDEFGHIJKLMNOPQRSTUV"); 10000000];
// do some kind of processing
}
الفعالية(مثال واقعي من طلب سحب) ¶
في المثال التالي، يُتعامل مع السلاسل النصية القصيرة باستخدام CompactString:
أعطى هذا تقليلًا في استخدام الذاكرة بحوالي 20٪.
حذف الحقول غير الضرورية في البُنى طويلة العمر¶
قد تؤثر البُنى التي تستمر في الاحتفاظ بها في الذاكرة أثناء بدء تشغيل العملية على إجمالي استخدام الذاكرة. في Hayabusa، يُحتفظ بالبُنى التالية (اعتبارًا من الإصدار 2.2.2) بأعداد كبيرة على وجه الخصوص.
كان لإزالة الحقول المرتبطة بالبُنى أعلاه بعض التأثير في تقليل إجمالي استخدام الذاكرة.
قبل ¶
على سبيل المثال، كان حقل DetectInfo، حتى الإصدار 1.8.1، على النحو التالي:
#[derive(Debug, Clone)]
pub struct DetectInfo {
pub rulepath: CompactString,
pub ruletitle: CompactString,
pub level: CompactString,
pub computername: CompactString,
pub eventid: CompactString,
pub detail: CompactString,
pub record_information: CompactString,
pub ext_field: Vec<(CompactString, Profile)>,
pub is_condition: bool,
}
بعد ¶
بحذف حقل record_information كما يلي
#[derive(Debug, Clone)]
pub struct DetectInfo {
pub rulepath: CompactString,
pub ruletitle: CompactString,
pub level: CompactString,
pub computername: CompactString,
pub eventid: CompactString,
pub detail: CompactString,
// remove record_information field
pub ext_field: Vec<(CompactString, Profile)>,
pub is_condition: bool,
}
الفعالية(مثال واقعي من طلب سحب) ¶
في المثال التالي، عند الاختبار على بيانات كان فيها عدد سجلات نتائج الكشف حوالي 1.5 مليون،
- Reduced memory usage of DetectInfo/EvtxRecordInfo #837
- Reduce memory usage by removing unnecessary regex #894
تمكنا من تحقيق تقليل في استخدام الذاكرة بحوالي 300 ميجابايت.
القياس المرجعي¶
استخدام دالة الإحصائيات الخاصة بمُخصِّص الذاكرة.¶
تحتفظ بعض مُخصِّصات الذاكرة بإحصائيات استخدام الذاكرة الخاصة بها. على سبيل المثال، في mimalloc، يمكن استدعاء الدالة mi_stats_print_out() للحصول على استخدام الذاكرة.
كيفية الحصول على الإحصائيات ¶
المتطلبات الأساسية: يجب أن تكون مستخدمًا لـ mimalloc كما هو موضح في قسم تغيير مُخصِّص الذاكرة.
- في قسم dependencies من
Cargo.toml، أضف حزمة libmimalloc-sys: - كلما أردت طباعة إحصائيات استخدام الذاكرة، اكتب الكود التالي، وداخل كتلة
unsafe، استدعِ mi_stats_print_out(). ستُخرَج إحصائيات استخدام الذاكرة إلى الإخراج القياسي. -
قيمة
peak/reservedفي أعلى اليسار هي الحد الأقصى لاستخدام الذاكرة.
مثال ¶
طُبِّق التنفيذ أعلاه في ما يلي:
في Hayabusa، إذا أضفت الخيار --debug، فسيتم إخراج إحصائيات استخدام الذاكرة في النهاية.
استخدام عداد الأداء في Windows¶
يمكن التحقق من استخدامات الموارد المختلفة من الإحصائيات التي يمكن الحصول عليها من جانب نظام التشغيل. في هذه الحالة، ينبغي ملاحظة النقطتين التاليتين.
- التأثير من برامج مكافحة الفيروسات (Windows Defender)
- يتأثر التشغيل الأول فقط بالفحص ويكون أبطأ، لذا فإن النتائج من التشغيل الثاني وما بعده بعد البناء مناسبة للمقارنة. (أو يمكنك تعطيل برنامج مكافحة الفيروسات للحصول على نتائج أكثر دقة.)
- التأثير من التخزين المؤقت للملفات
- تكون النتائج من المرة الثانية وما بعدها بعد بدء تشغيل نظام التشغيل أسرع من المرة الأولى لأن evtx وعمليات الإدخال/الإخراج الأخرى للملفات تُقرأ من ذاكرة التخزين المؤقت للملفات في الذاكرة، لذا فإن النتائج من المرة الأولى بعد إقلاع نظام التشغيل أكثر مثالية لأخذ القياسات المرجعية.
كيفية الحصول عليها ¶
المتطلبات الأساسية:الإجراء التالي صالح فقط للبيئات التي يكون فيها PowerShell 7 مثبتًا بالفعل على Windows.
- أعد تشغيل نظام التشغيل
- شغّل أمر Get-Counter الخاص بـ
PowerShell 7والذي سيسجل باستمرار عداد الأداء كل ثانية إلى ملف CSV. (إذا كنت ترغب في قياس موارد غير المذكورة أدناه، فإن هذه المقالة مرجع جيد.) - نفّذ العملية التي تريد قياسها.
مثال ¶
يحتوي ما يلي على مثال لإجراء قياس الأداء باستخدام Hayabusa.
استخدام heaptrack¶
heaptrack هو محلل ذاكرة متطور متاح لنظامي Linux وmacOS. باستخدام heaptrack، يمكنك التحقيق بدقة في الاختناقات.
كيفية الحصول عليها ¶
المتطلبات الأساسية: فيما يلي الإجراء لنظام Ubuntu 22.04. لا يمكنك استخدام heaptrack على Windows.
- ثبّت heaptrack باستخدام الأمرين التاليين.
- أزل كود mimalloc التالي من Hayabusa. (لا يمكنك استخدام محلل ذاكرة heaptrack مع mimalloc.
- https://github.com/Yamato-Security/hayabusa/blob/v2.2.2/src/main.rs#L32-L33
- https://github.com/Yamato-Security/hayabusa/blob/v2.2.2/src/main.rs#L59-L60
-
https://github.com/Yamato-Security/hayabusa/blob/v2.2.2/src/main.rs#L632-L634
-
احذف قسم [profile.release] في ملف
Cargo.tomlالخاص بـ Hayabusa وغيّره إلى ما يلي: -
ابنِ بناءً للإصدار:
cargo build --release - شغّل
heaptrack hayabusa csv-timeline -d sample -o out.csv
الآن عندما تنتهي Hayabusa من العمل، ستُفتح نتائج heaptrack تلقائيًا في تطبيق ذي واجهة رسومية.
أمثلة ¶
يُعرض مثال على نتائج heaptrack أدناه. تتيح لك علامتا التبويب Flame Graph وTop-Down التحقق بصريًا من الدوال ذات استخدام الذاكرة المرتفع.
المراجع¶
المساهمات¶
يستند هذا المستند إلى نتائج من حالات تحسين فعلية في Hayabusa. إذا وجدت أي أخطاء أو تقنيات يمكن أن تحسن الأداء، يرجى إرسال مشكلة أو طلب سحب إلينا.


