דגימת מעבד וזמן חסום¶
האחוז הגדול אינו בהכרח הבעיה הגדולה¶
נניח שפרופיל מעבד מראה שפונקציה אחת אחראית לשמונים אחוז מהדגימות. קל להסיק שצריך להאיץ אותה כדי לקצר את זמן הבקשה. אבל אם התהליך נמצא על המעבד רק עשירית מזמן הבקשה, רוב ההשהיה נמצאת במקום אחר. אחוז מתוך זמן פעיל אינו אחוז מתוך זמן הקיר שהלקוח חיכה.
נפרק את זמן הבקשה לשלושה סוגים שימושיים: זמן שבו המשימה רצה, זמן שבו היא מוכנה לרוץ אך מחכה למעבד, וזמן שבו היא ממתינה לתנאי אחר. החלוקה אינה תמיד פשוטה במדידה, אבל היא מונעת ערבוב בין חישוב יקר, תחרות על המעבד והמתנה לנתונים או לנעילה.
עומס שאפשר לשנות בו גורם אחד¶
נבנה תוכנית עם מסלול חישוב ושינה ידועה. מצב אחד משקיע יותר בחישוב, והשני מבצע מעט חישוב וממתין בכל בקשה. נשמור את הקוד בשם latency_probe.rs. הדוגמה מדפיסה מזהה תהליך כדי שנוכל לסנן תצפיות תזמון, ואינה מדפיסה בכל איטרציה כדי שלא להפוך את הפלט לחלק העיקרי של העומס.
use std::hint::black_box;
use std::time::{Duration, Instant};
#[inline(never)]
fn crunch(rounds: u64) -> u64 {
let mut state = 1_u64;
for _ in 0..rounds {
state = black_box(state.wrapping_mul(2862933555777941757)
.wrapping_add(3037000493));
}
state
}
#[inline(never)]
fn request(rounds: u64, delay: Duration) -> (u128, u64) {
let begin = Instant::now();
let value = crunch(rounds);
if !delay.is_zero() { std::thread::sleep(delay); }
(begin.elapsed().as_micros(), value)
}
fn main() {
let mode = std::env::args().nth(1).unwrap_or_else(|| "wait".into());
let (rounds, delay) = match mode.as_str() {
"cpu" => (2_000_000, Duration::ZERO),
"wait" => (1_000, Duration::from_millis(20)),
_ => panic!("expected cpu or wait"),
};
println!("pid={}", std::process::id());
let started = Instant::now();
let mut samples = Vec::new();
let mut checksum = 0_u64;
for _ in 0..100 {
let (elapsed, value) = request(rounds, delay);
samples.push(elapsed);
checksum ^= value;
}
samples.sort_unstable();
println!("wall={:?} p50_us={} p99_us={} checksum={checksum}",
started.elapsed(), samples[49], samples[98]);
}
נבנה עם אופטימיזציה, מידע סמלים ושמירת מצביעי מסגרת. הדגל האחרון מסייע לשחזור שרשרת הקריאות במנגנון שנבחר בניסוי. שמירתו רק בקובץ שלנו אינה מבטיחה שכל ספרייה חיצונית נבנתה באותה צורה.
rustc --edition=2024 -C opt-level=2 -C debuginfo=2 \
-C force-frame-pointers=yes latency_probe.rs -o latency_probe
perf stat -r 5 -e task-clock,context-switches,cpu-migrations,page-faults \
-- ./latency_probe wait
perf stat -r 5 -e task-clock,context-switches,cpu-migrations,page-faults \
-- ./latency_probe cpu
נשווה זמן משימה לזמן הקיר ולא רק בין שתי רשימות מונים. במצב ההמתנה נצפה לחלק קטן יותר של זמן פעיל ביחס לזמן הקיר. ערכים מדויקים תלויים במחשב ובעומס סביבו, ולכן נשמור את התוצאות בפועל. אם הרשאות הביצועים או תמיכת הקרנל חוסמות מדידה, נתעד זאת ונבדוק את תצורת המעבדה במקום להמציא ערכים.
דגימות ושרשרת קריאות¶
נאסוף דגימות של זמן מעבד במרחב המשתמש. ניצור קובץ נפרד לכל מצב וננתח את שניהם עם אותם סמלים. תדירות הדגימה שבחרנו מתאימה לניסוי קטן, ואינה כלל קבוע לכל שירות.
perf record -o wait.perf -e cpu-clock:u -F 99 --call-graph fp \
-- ./latency_probe wait
perf report -i wait.perf --stdio
perf record -o cpu.perf -e cpu-clock:u -F 99 --call-graph fp \
-- ./latency_probe cpu
perf report -i cpu.perf --stdio
במצב ההמתנה ייתכן שמספר הדגימות יהיה קטן מאוד, מפני שהתהליך אינו פעיל רוב הזמן. אחוז גבוה של מעט דגימות אינו בסיס חזק לאומדן מדויק. נוכל להאריך את הניסוי בלי לשנות את העבודה לכל בקשה, ונציין את מספר הדגימות לצד האחוזים.
הפרופיל מייחס דגימה למקום שבו התהליך נמצא בזמן אירוע הדגימה. עלות עצמית של פונקציה שונה מעלות הכוללת קריאות לבנות שלה. כאשר מסתכלים על תרשים מחסניות, רוחב ענף מייצג משקל דגימות לפי שיטת האיסוף, ולא בהכרח מספר קריאות או משך של קריאה בודדת. נבדוק את ההגדרה לפני שנבחר פונקציה לשינוי.
שחזור מחסנית אינו מובן מאליו¶
אפשר להשתמש במצביעי מסגרת, במידע ניפוי או במנגנונים חומרתיים מתאימים לשחזור שרשרת. לכל שיטה עלות ותנאי תמיכה. מידע חסר יכול לייצר מחסנית קטועה, וסמלים חסרים יכולים להפוך כתובות לשמות לא מועילים. נבדוק כמה דגימות איבדו מסגרות לפני שנפרש את הענפים הקצרים כאילו היו מסלולים אמיתיים.
אופטימיזציה יכולה לשלב פונקציות, להסיר קריאות או לשנות את גבולותיהן. מידע ניפוי עשוי לתאר גם מסגרות משולבות, אך אין להניח ששורת מקור אחת שקולה להוראה אחת. כאשר שינוי קטן נראה כמו שיפור עצום, נבדוק שהמהדר לא הסיר את העבודה שאנו חושבים שמדדנו.
מדידת המתנה ותור ריצה¶
נאסוף אירועי תזמון במכונת המעבדה באמצעות תת־הכלי של המתזמן. האיסוף יכול לדרוש הרשאות נוספות ולכלול משימות אחרות, ולכן נסנן לפי מזהה התהליך שהודפס. הדוח צריך לשמור את ההקשר המערכתי הדרוש להסבר התחרות.
sudo perf sched record -o schedule.perf -- ./latency_probe wait
sudo perf sched timehist -i schedule.perf
נזהה מתי המשימה יצאה מהמעבד, מתי הפכה שוב למוכנה ומתי באמת חזרה לרוץ. ההפרש בין מוכנות לריצה מצביע על עיכוב תזמון, בעוד הזמן לפני המוכנות יכול להיות המתנה לשעון, לקלט או למשאב. בתוכנית שלנו ההמתנה המכוונת מספקת אמת מידה שמאפשרת לבדוק אם אנו מפרשים נכון את האירועים.
פרופיל זמן חסום מצמיד משך המתנה למסלול שבו המשימה נחסמה. הוא יכול להבליט נעילה שאינה מופיעה כמעט בפרופיל המעבד. עם זאת גם כאן צריך לדעת איזה אירוע מסמן התחלה וסיום, וכיצד מטפלים במשימה שיצאה מהמעבד אך נשארה מוכנה. איסוף שמערבב שינה עם המתנה בתור ריצה יוביל לתיקון הלא נכון.
מוני חומרה עם משמעות מוגדרת¶
נוכל להוסיף מונים כגון מחזורים, הוראות והחטאות מטמון אם המעבד וסביבת ההרצה חושפים אותם. במכונה וירטואלית ייתכן שהמונים אינם זמינים או שהתמיכה חלקית. כאשר כמה אירועים מתחרים על מספר מוגבל של מונים, הכלי עשוי לחלק ביניהם את זמן המדידה ולהציג אומדן מותאם. נבדוק את זמן הריצה היחסי של כל מונה ולא נתייחס לכל מספר כאילו נמדד ברציפות.
יחס הוראות למחזור אינו מדד אוניברסלי לטיב התוכנית. גרסה יכולה לבצע פחות עבודה ולהציג יחס נמוך יותר, ועדיין לסיים מהר יותר. החטאות מטמון יכולות לעלות בעקבות שינוי שמשפר מקביליות. נבחר מדד עיקרי המתאים למטרה ונשתמש במונים להסבר מנגנון, במקום לחפש מספר יחיד שצריך תמיד לעלות או לרדת.
לבדוק את התיקון מחוץ לכלי¶
לאחר שינוי נסיר את כלי האיסוף ונחזור למדידת זמן התגובה והקצב של התוכנית. נוודא שהשיפור מופיע גם בעומס הרלוונטי ושלא גדלו צריכת הזיכרון או זמן ההמתנה של בקשות אחרות. תוצאה בפרופיל היא ראיה לבחירת שינוי, אך המשתמש מרגיש את ההתנהגות של המערכת כולה.
פרטי האיסוף מופיעים בתיעוד המונים, בתיעוד הדגימה ובתיעוד מדידת המתזמן. בכל גרף נציין את יחידת המשקל ואת תחום האיסוף, כדי שאחוז של זמן פעיל לא יוצג בטעות כאחוז מזמן הבקשה.