Debugging ба Profiling
Програмчлалын алтан дүрэм бол код нь таны бодсон шиг биш, харин таны хэлсэн шиг ажилладагт оршино. Тэрхүү зөрүүг арилгах нь заримдаа нэлээд хэцүү ажил байж болно. Энэ лекцээр бид алдаатай (buggy) болон нөөц их шаарддаг кодуудтай ажиллах хүчин төгөлдөр арга техникүүд болох дебаг хийх (debugging) болон профилинг (profiling)-ийг үзэх болно.
Debugging
Printf Debugging болон Logging
“Алдааг засах хамгийн үр дүнтэй хэрэгсэл бол анхааралтай тунгаан бодох болон зөв газарт нь тавьсан print тушаалууд хэвээр байна.” — Brian Kernighan, Unix for Beginners.
Програмыг дебаг хийх анхны бөгөөд энгийн арга бол асуудал илэрсэн хэсгийн орчимд print тушаалуудыг (print statements) нэмж, асуудал юунаас болж байгааг ойлгох хангалттай мэдээлэл цуглуулах хүртлээ дахин дахин ажиллуулж шалгах явдал юм.
Хоёр дахь арга бол кодондоо дурын print тушаал нэмэхийн оронд лог хөтлөх (logging) явдал юм. Лог хөтлөх нь үндсэндээ “илүү анхааралтай хэвлэх” үйлдэл бөгөөд ихэвчлэн дараах бэлэн боломжууд бүхий лог сангуудыг (logging framework) ашигладаг:
- логуудыг (эсвэл логуудын тодорхой хэсгийг) бусад байршил руу (файл гэх мэт) чиглүүлэх боломж;
- аюултай байдлын зэргүүдийг (severity levels: INFO, DEBUG, WARN, ERROR гэх мэт) тохируулж, тэдгээрийн дагуу гаралтыг шүүх боломж;
- логтой холбоотой өгөгдлийг бүтэцлэгдсэн лог (structured logging) хэлбэрээр хадгалах бөгөөд ингэснээр дараа нь тэдгээрийг задлан шинжлэхэд хялбар болдог.
Програм бичих явцдаа урьдчилан лог бичих тушаалуудыг тавьж өгснөөр дебаг хийхэд хэрэгтэй өгөгдөл бэлэн байх давуу талтай! Мөн print тушаал ашиглан асуудлыг олоод зассаныхаа дараа тэдгээрийг устгахын оронд зөв лог тушаал болгон үлдээх нь зүйтэй. Ингэснээр ирээдүйд ижил алдаа гарахад кодыг өөрчлөх шаардлагагүйгээр оношилгооны мэдээлэл бэлэн байх болно.
Гуравдагч талын логууд (Third-party logs): Маш олон програмууд ажиллахдаа илүү дэлгэрэнгүй мэдээлэл хэвлэх
-vэсвэл--verboseтугуудыг дэмждэг. Энэ нь тушаал яагаад алдаа заасныг илрүүлэхэд маш хэрэгтэй байдаг. Зарим нь бүр илүү дэлгэрэнгүй мэдээлэл авахын тулд тугийг давтан ашиглахыг зөвшөөрдөг. Үйлчилгээнүүдийн (өгөгдлийн сан, вэб сервер гэх мэт) алдааг засахдаа тэдгээрийн логуудыг шалгаарай — Linux дээр ихэвчлэн/var/log/хавтаст байдаг. Systemd үйлчилгээнүүдийн логийг харахдааjournalctl -u <service>ашиглана уу. Гуравдагч талын сангуудын (libraries) хувьд орчны хувьсагч (environment variables) эсвэл тохиргоогоор дамжуулан дебаг логийг (debug logging) идэвхжүүлэх боломжтой эсэхийг шалгаарай.
Debugger-үүд
Хэвлэж дебаг хийх (print debugging) нь юуг хэвлэхээ мэдэж байгаа болон кодыг амархан өөрчилж дахин ажиллуулах боломжтой үед сайн тохирдог. Харин ямар мэдээлэл хэрэгтэй байгаагаа мэдэхгүй байх, алдаа нь дахин гаргахад маш хэцүү нөхцөлд илэрдэг, эсвэл кодыг өөрчилж програмыг дахин эхлүүлэх нь маш өндөр өртөгтэй (ачаалах хугацаа урт, төлөвийг дахин үүсгэхэд нарийн төвөгтэй гэх мэт) үед debugger-үүд маш чухал үүрэг гүйцэтгэдэг.
Debugger нь програмын ажиллагааг шууд хянах боломжийг олгодог програм бөгөөд дараах үйлдлүүдийг хийж чадна:
- Кодын тодорхой мөрөнд хүрэхэд ажиллагааг зогсоох (breakpoint).
- Тушаалуудыг нэг нэгээр нь алхам алхмаар ажиллуулах.
- Програм гацсаны (crash) дараа хувьсагчдын утгыг шалгах.
- Тодорхой нөхцөл биелэх үед ажиллагааг зогсоох (conditional breakpoint).
- Мөн бусад олон нарийн, ахисан түвшний үйлдлүүд.
Ихэнх програмчлалын хэлнүүд debugger-ийг дэмждэг эсвэл хамт ирдэг. Хамгийн уян
хатан нь ямар ч native binary-г дебаг хийж чаддаг gdb
(GNU Debugger) болон lldb (LLVM Debugger) зэрэг
ерөнхий зориулалттай debugger-үүд юм. Мөн олон хэлнүүд ажиллах орчинтойгоо
(runtime) илүү нягт холбогддог өөрийн хэлний тусгайлсан debugger-тэй байдаг
(Python-ий pdb эсвэл Java-ийн jdb гэх мэт).
gdb нь C, C++, Rust болон бусад хөрвүүлэгддэг (compiled) хэлнүүдийн хувьд
де-факто стандарт debugger юм. Энэ нь бараг ямар ч процессыг хянаж, одоогийн
машины төлөв: регистрүүд (registers), стек (stack), програмын заагч (program
counter) зэргийг харах боломжийг олгодог.
Зарим хэрэгцээт GDB тушаал:
run- Програмыг эхлүүлэхb {function}эсвэлb {file}:{line}- Breakpoint тавихc- Ажиллагааг үргэлжлүүлэхstep/next/finish- Алхам алхмаар дотогш орох / дээгүүр нь алхах / гарахp {variable}- Хувьсагчийн утгыг хэвлэхbt- Backtrace (call stack) харуулахwatch {expression}- Утга нь өөрчлөгдөх үед ажиллагааг зогсоох (watchpoint тавих)
Командын мөрийн хажуугаар эх кодыг хамт харахын тулд GDB-ийн TUI горимыг (
gdb -tuiэсвэл GDB доторCtrl-x aдарах) ашиглаж үзээрэй.
Record-Replay Debugging
Хамгийн ядаргаатай алдаануудын нэг бол Heisenbugs буюу тэдгээрийг ажиглахыг оролдох үед алга болдог эсвэл зангаа хувиргадаг алдаанууд юм. Уралдааны нөхцөл (race conditions), хугацаанаас хамаарсан алдаанууд болон зөвхөн системийн тодорхой нөхцөлд гардаг алдаанууд үүнд багтана. Програмыг дахин ажиллуулахад өөр үр дүн гардаг тул (жишээ нь, print тушаал нэмэх нь кодын ажиллагааг хангалттай удаашруулж, уралдааны нөхцөл үүсэхээ болих) уламжлалт аргаар засах нь үр дүнгүй байдаг.
Record-replay debugging нь програмын ажиллагааг бичиж аваад, түүнийг хэдэн ч удаа яг ижилхэн, детерминистик (deterministic) байдлаар дахин ажиллуулах боломжийг олгосноор үүнийг шийддэг. Тэр ч байтугай, асуудал хаана гарсныг олохын тулд ажиллагааг ухрааж (reverse) үзэж болно.
rr нь Linux-д зориулсан маш хүчирхэг хэрэгсэл бөгөөд програмын ажиллагааг бичиж аваад, бүрэн дебаг хийх боломжтойгоор детерминистик байдлаар дахин ажиллуулдаг. Энэ нь GDB-тэй хамтарч ажилладаг тул та интерфейсийг нь аль хэдийн мэднэ.
Үндсэн хэрэглээ:
# Програмын ажиллагааг бичиж авах
rr record ./my_program
# Бичлэгийг дахин ажиллуулах (GDB-ийг нээнэ)
rr replay
Шидтэй хэсэг нь дахин ажиллуулах явцад болдог. Ажиллагаа нь детерминистик тул та ухрааж дебаг хийх (reverse debugging) тушаалуудыг ашиглаж болно:
reverse-continue(rc) - Breakpoint-д тултал ухрааж ажиллуулахreverse-step(rs) - Нэг мөрөөр ухрахreverse-next(rn) - Функцийн дуудлагыг алгасан ухрахreverse-finish- Одоогийн функц рүү орох хүртэл ухрааж ажиллуулах
Энэ нь дебаг хийхэд үнэхээр хүчирхэг боломж юм. Програм гацсан үед хаана алдаа гарсныг тааж breakpoint тавихын оронд та:
- Гацах цэг хүртэл ажиллуулна
- Эвдэрсэн төлөвийг шалгана
- Эвдэрсэн хувьсагч дээр watchpoint тавина
reverse-continueажиллуулж хаана уг хувьсагчийн утга буруу өөрчлөгдсөнийг (corrupted) яг таг олно
rr-ийг хэзээ ашиглах вэ:
- Хааяа нэг унадаг тогтворгүй тестүүд (flaky tests)
- Уралдааны нөхцөл (race conditions) болон олон урсгалт (threading) кодын алдаа
- Дахин гаргахад хэцүү гацалтууд
- “Цаг хугацааг ухраахыг” хүссэн ямар ч алдаа
Тэмдэглэл: rr нь зөвхөн Linux дээр ажилладаг бөгөөд техник хангамжийн гүйцэтгэлийн тоолууруудыг (hardware performance counters) шаарддаг. Эдгээр тоолуурыг ил гаргадаггүй виртуал машинууд дээр (жишээ нь, ихэнх AWS EC2 instance-ууд) ажиллахгүй бөгөөд GPU хандалтыг дэмждэггүй. macOS-ийн хувьд Warpspeed-ийг сонирхоорой.
rr ба зэрэгцээ ажиллагаа (concurrency): rr нь ажиллагааг детерминистик байдлаар бичдэг тул урсгалуудын хуваарилалтыг (thread scheduling) цуврал болгодог. Энэ нь зарим уралдааны нөхцөлүүд rr дор ажиллах үед илрэхгүй байж магадгүй гэсэн үг юм. Гэхдээ rr нь уралдааны нөхцөлийг дебаг хийхэд хэрэгтэй хэвээр байна — нэгэнт алдаатай ажиллагааг бичиж авсан бол түүнийг найдвартай дахин ажиллуулж болно. Гэхдээ тогтворгүй алдааг барьж авахын тулд хэд хэдэн удаа бичиж авах оролдлого хийх шаардлагатай байж магадгүй. Зэрэгцээ ажиллагаанаас бусад алдааны хувьд rr нь хамгийн шилдэг нь юм: та ажиллагааг яг таг дахин үүсгэж, ухрааж дебаг хийх тушаалаар санах ойн эвдрэлийг (corruption) мөшгөх боломжтой.
System Call Tracing
Заримдаа програм тань үйлдлийн системтэй хэрхэн харилцаж байгааг ойлгох шаардлагатай болдог. Програмууд нь цөмөөс (kernel) үйлчилгээ авахын тулд систем дуудлага (system calls) хийдэг — файл нээх, санах ой хуваарилах, процесс үүсгэх гэх мэт. Эдгээр дуудлагыг мөрдөх нь програм яагаад гацсан (hanging) байгаа, ямар файл руу хандахыг оролдож байгаа, эсвэл хаана хүлээж цаг алдаж байгааг харуулж чаддаг.
strace (Linux) болон dtruss (macOS)
strace нь програмын
хийж буй систем дуудлага бүрийг ажиглах боломжийг олгодог:
# Бүх систем дуудлагыг хянах
strace ./my_program
# Зөвхөн файлтай холбоотой дуудлагуудыг хянах
strace -e trace=file ./my_program
# Хүү процессуудыг хянах (бусад програмыг ажиллуулдаг програмуудын хувьд чухал)
strace -f ./my_program
# Ажиллаж байгаа процессыг хянах
strace -p <PID>
# Хугацааны мэдээллийг харуулах
strace -T ./my_program
macOS болон BSD дээр ижил төстэй ажиллагаанд (
dtrace-ийг ороодог)dtruss-ийг ашиглана уу.
strace-ийг илүү гүнзгий судлахыг хүсвэл Julia Evans-ийн гайхалтай strace zine-ийг уншина уу.
bpftrace болон eBPF
eBPF (extended Berkeley Packet Filter) нь Linux цөм (kernel)
дотор хамгаалагдсан (sandboxed) програмуудыг ажиллуулах боломжийг олгодог хүчирхэг
технологи юм. bpftrace нь eBPF програмуудыг
бичих дээд түвшний синтаксийг санал болгодог. Эдгээр нь цөм дотор ажилладаг
тул маш өндөр хүчин чадалтай бөгөөд ямар систем дуудлагууд хийгдэж байгааг тоолох,
хугацааг хэмжих, аргументуудыг шалгахад ашиглагддаг.
# Системийн хэмжээнд файл нээх үйлдлүүдийг хянах (шууд хэвлэнэ)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
# Систем дуудлагуудыг нэрээр нь тоолох (Ctrl-C хийхэд нэгтгэлийг хэвлэнэ)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
Та мөн bcc хэрэгслийг ашиглан C хэл дээр eBPF
програмуудыг шууд бичиж болох бөгөөд үүнд дискний үйлдлийн хоцролтыг хэмждэг biosnoop,
нээгдсэн файлуудыг хэвлэдэг opensnoop зэрэг олон бэлэн хэрэгслүүд
багтсан байдаг.
strace нь хурдан ажиллуулж эхлэхэд хялбар байдгаараа хэрэгцээтэй байдаг бол bpftrace
нь overhead багатай ажиллах, цөмийн функцуудыг хянах, нэгтгэсэн тооцоолол хийх
шаардлагатай үед илүү тохиромжтой. bpftrace-ийг ажиллуулахад root эрх хэрэгтэй
бөгөөд тэрээр тодорхой процесс биш, харин системийн хэмжээнд цөмийг хянадаг.
Тодорхой програмыг хянахын тулд командын нэр эсвэл PID-ээр шүүж болно:
# Командын нэрээр шүүх (Ctrl-C хийхэд үр дүнг хэвлэнэ)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /comm == "bash"/ { @[probe] = count(); }'
# -c ашиглан шинээр эхэлж буй тодорхой тушаалыг хянах (cpid = child PID)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /pid == cpid/ { @[probe] = count(); }' -c 'ls -la'
-c туг нь заасан тушаалыг ажиллуулаад cpid-ийг түүний PID-ээр оноодог бөгөөд
энэ нь програмыг ажиллаж эхлэх мөчөөс нь хянахад маш тохиромжтой. Програм дуусахад
bpftrace нэгтгэсэн үр дүнг хэвлэх болно.
Network Debugging
Сүлжээний асуудлуудын хувьд tcpdump
болон Wireshark нь сүлжээний пакетуудыг барьж аваад
шинжлэх боломжийг олгодог:
# 80-р порт дээрх пакетуудыг барьж авах
sudo tcpdump -i any port 80
# Wireshark-аар шинжлэхийн тулд пакетуудыг файлд хадгалах
sudo tcpdump -i any -w capture.pcap
HTTPS урсгалын хувьд шифрлэлтээс шалтгаалан tcpdump ашиглах нь үр дүнгүй байдаг. mitmproxy зэрэг хэрэгслүүдийг дундын прокси (intercepting proxy) болгон ашиглаж шифрлэгдсэн урсгалыг шалгаж болно. Вэб програмуудын хувьд хөтчийн хөгжүүлэгчийн хэрэгсэл (Developer Tools - Network tab) нь HTTPS хүсэлтийг дебаг хийх хамгийн хялбар арга бөгөөд тэнд шифрлэгдээгүй өгөгдөл, толгой мэдээлэл (headers) болон хугацааг харуулдаг.
Memory Debugging
Санах ойн алдаанууд — буфер халилт (buffer overflows), устгасны дараа санах ой ашиглах (use-after-free), санах ойн алдагдал (memory leaks) — нь хамгийн аюултай бөгөөд дебаг хийхэд хэцүү алдааны ангилалд багтдаг. Тэд ихэвчлэн шууд гацдаггүй бөгөөд санах ойг эвдэн дараа нь өөр газар асуудал үүсгэдэг.
Sanitizers
Санах ойн алдааг олох нэг арга бол програмыг ажиллаж байх явцад алдааг олох боломжийг бүрдүүлдэг хөрвүүлэгчийн (compiler) онцлог болох sanitizers-ийг ашиглах явдал юм. Жишээлбэл, өргөн хэрэглэгддэг AddressSanitizer (ASan) нь дараах алдаануудыг илрүүлдэг:
- Buffer overflows (стек, хийп болон глобал хэмжээнд)
- Use-after-free
- Use-after-return
- Memory leaks
# AddressSanitizer-тай хөрвүүлэх
gcc -fsanitize=address -g program.c -o program
./program
Дараах хэрэгцээт sanitizer-ууд байдаг:
- ThreadSanitizer (TSan): Олон урсгалт кодын өгөгдлийн уралдааныг (data races)
илрүүлнэ (
-fsanitize=thread) - MemorySanitizer (MSan): Эхлүүлээгүй (uninitialized) санах ой унших үйлдлийг
илрүүлнэ (
-fsanitize=memory) - UndefinedBehaviorSanitizer (UBSan): Бүхэл тооны халилт зэрэг тодорхойгүй
ажиллагаануудыг (undefined behavior) илрүүлнэ (
-fsanitize=undefined)
Sanitizer-ууд нь кодыг дахин хөрвүүлэхийг (recompilation) шаарддаг боловч CI систем болон өдөр тутмын хөгжүүлэлтийн явцад ажиллуулахад хангалттай хурдан байдаг.
Valgrind: Дахин хөрвүүлэх боломжгүй үед
Valgrind нь санах ойн алдааг олохын тулд таны програмыг виртуал машин шиг орчинд ажиллуулдаг. Энэ нь sanitizer-уудаас удаан боловч дахин хөрвүүлэхийг шаарддаггүй:
valgrind --leak-check=full ./my_program
Valgrind-ийг хэзээ ашиглах вэ:
- Танд эх код (source code) байхгүй үед
- Та дахин хөрвүүлж чадахгүй байгаа үед (гуравдагч талын сангууд)
- Танд sanitizer-т байхгүй тусгай хэрэгсэл хэрэгтэй үед
Valgrind нь үнэндээ маш хүчирхэг хяналттай ажиллуулах орчин бөгөөд бид үүнийг профилинг хэсэгт дахин харах болно!
AI-ийг Debugging-д ашиглах
Том хэмжээний хэлний загварууд (LLM) нь дебаг хийхэд гайхалтай хэрэгтэй туслагч болж байна. Тэд уламжлалт хэрэгслүүдийг нөхөх зарим дебаг хийх үйлдлүүдэд маш сайн байдаг.
LLM-үүдийн сайн хийж чаддаг зүйлс:
- Ойлгомжгүй алдааны мэдээллийг тайлбарлах: Хөрвүүлэгчийн алдаанууд, ялангуяа C++ template эсвэл Rust-ийн borrow checker-ийн алдааны мэдээллүүд маш ойлгомжгүй байдаг. LLM-үүд тэдгээрийг энгийн хэлээр тайлбарлаж, засах аргыг санал болгож чаддаг.
- Хэл болон абстракцийн зааг дээр ажиллах: Хэрэв та олон хэл дамжсан асуудлыг (жишээ нь, Python-оос дуудаж буй C сангийн алдаа) засаж байгаа бол LLM нь эдгээр давхаргуудын хооронд шилжихэд тусална. Тэд FFI зааг, build системийн асуудал болон олон хэлний дебаг хийхэд маш сайн байдаг (жишээ нь: миний програмд алдаа гарсан ч энэ нь миний хамааралтай сангуудын аль нэгний алдаанаас болсон гэж үзэж байгаа үед).
- Шинж тэмдгийг үндсэн шалтгаантай холбох: “Програм зөв ажиллаж байгаа ч хүлээж байснаас 10 дахин их санах ой ашиглаж байна” гэх мэт тодорхой бус шинж тэмдгүүдийг судлахад LLM тусалж, юуг шалгах талаар зөвлөгөө өгч чадна.
- Crash dump болон stack trace-ийг шинжлэх: Stack trace-ийг хуулж тавиад юунаас болж алдаа гарсныг асууж болно.
Дебаг тэмдэгтүүд оруулах тухай (Debug symbols): Хэрэгцээтэй stack trace болон дебаг хийхийн тулд өөрийн binary файлыг (болон холбогдох сангуудыг) дебаг тэмдэгтүүдтэй (
-gтуг) хөрвүүлэх шаардлагатай. Дебаг мэдээлэл ихэвчлэн DWARF форматыг ашигладаг. Үүнээс гадна, frame pointers-тэй (-fno-omit-frame-pointer) хөрвүүлэх нь stack trace-ийг, ялангуяа хурд хэмжигч (profiling) хэрэгслүүдийн хувьд илүү найдвартай болгодог. Эдгээр байхгүй тохиолдолд stack trace нь зөвхөн санах ойн хаягуудыг харуулах эсвэл дутуу байх аюултай. Энэ нь Python эсвэл Java-аас илүү хөрвүүлэгддэг хэлнүүдийн хувьд маш чухал юм.
Анхаарах хязгаарлалтууд:
- LLM-үүд үнэн мэт сонсогдох боловч буруу тайлбар зохиож болно
- Тэд алдааг засахын оронд зөвхөн шинж тэмдгийг дарах код санал болгож магадгүй
- Санал болгосон засварыг бодит дебаг хэрэгслээр үргэлж шалгаж байгаарай
- Тэд таныг кодоо ойлгохыг орлох биш, харин туслах үүрэгтэйг санаарай
Энэ нь Хөгжүүлэлтийн орчин лекцээр үзсэн ерөнхий AI код бичих боломжоос тусдаа юм. Энд бид зөвхөн LLM-ийг дебаг хийх туслах болгон ашиглах талаар ярьж байна.
Profiling
Таны код функциональ байдлаар зөв ажиллаж байсан ч тэрээр процессор (CPU) болон санах ойг бүхэлд нь ашиглаад байвал хангалтгүй юм. Алгоритмын хичээлээр ихэвчлэн том O тэмдэглэгээг заадаг боловч програмын хамгийн их ачаалалтай хэсгийг (hot spots) хэрхэн олохыг заадаггүй. Хэт эрт оновчтой болгох нь бүх гай гамшгийн үндэс тул та profiler болон хянах хэрэгслүүдийг сурах хэрэгтэй. Тэд танд програмын аль хэсэг хамгийн их цаг болон нөөцийг зарцуулж байгааг ойлгоход туслах бөгөөд ингэснээр та зөвхөн тэр хэсгийг оновчтой болгоход анхаарлаа хандуулах боломжтой болно.
Хугацаа хэмжих
Гүйцэтгэлийг хэмжих хамгийн энгийн арга бол хугацааг хэмжих явдал юм. Олон нөхцөлд кодын хоёр цэгийн хооронд цагийг хэвлэхэд л хангалттай байдаг.
Гэвч wall clock цаг (бодит цаг) нь төөрөгдөл үүсгэж болно, учир нь компьютер
тань нэгэн зэрэг өөр процессуудыг ажиллуулж байгаа эсвэл сүлжээний хариу хүлээж
байж болно. time команд нь Real (Бодит), User (Хэрэглэгчийн), болон
Sys (Системийн) цагийг ялгаж харуулдаг:
- Real - Эхлэхээс төгсгөл хүртэлх бодит цаг, хүлээсэн цагийг багтаана
- User - Хэрэглэгчийн код ажиллуулахад CPU-ийн зарцуулсан цаг
- Sys - Цөмийн код ажиллуулахад CPU-ийн зарцуулсан цаг
$ time curl https://missing.csail.mit.edu &> /dev/null
real 0m0.272s
user 0m0.079s
sys 0m0.028s
Энд хүсэлт нь нийт 300 орчим миллисекунд үргэлжилсэн боловч CPU-ийн цаг нь ердөө 107ms байна (user + sys). Үлдсэн хугацаа нь сүлжээг хүлээхэд зарцуулагдсан байна.
Нөөцийг хянах
Заримдаа програмынхаа гүйцэтгэлийг шинжлэх эхний алхам бол түүний бодит нөөцийн хэрэглээг ойлгох явдал байдаг. Програмууд нөөцийн дутагдалд орох үедээ ихэвчлэн удаан ажилладаг.
- Ерөнхий хяналт:
htopнь ажиллаж буй процессуудын янз бүүрийн статистикийг харуулдагtopкомандын сайжруулсан хувилбар юм. Хэрэгцээт товчлуурууд:<F6>процессуудыг эрэмбэлэх,tмод бүтэц харуулах,hурсгалуудыг (threads) харах/нуух. Мөн үүнээс ч илүү олон зүйлийг хянадагbtopбайдаг. - Дискний I/O үйлдлүүд:
iotopнь дискний оролт/гаралтын (I/O) үйлдлүүдийг шууд харуулдаг. - Санах ойн хэрэглээ:
freeнь нийт сул болон ашиглагдаж буй санах ойг харуулна. - Нээлттэй файлууд:
lsofнь процессуудын нээсэн файлуудын мэдээллийг жагсаана. Аль процесс тодорхой файлыг нээснийг шалгахад маш хэрэгтэй. - Сүлжээний холболтууд:
ssнь сүлжээний холболтыг хянах боломжийг олгоно. Түгээмэл хэрэглээ бол аль процесс тодорхой портыг ашиглаж байгааг олох явдал юм:ss -tlnp | grep :8080. - Сүлжээний хэрэглээ:
nethogsболонiftopнь процесс бүрийн сүлжээний хэрэглээг хянах интерактив CLI хэрэгслүүд юм.
Гүйцэтгэлийн өгөгдлийг дүрслэх
Хүмүүс тооны хүснэгтээс илүү график дээрх зүй тогтлыг маш хурдан олж хардаг. Гүйцэтгэлийг шинжлэх үед өгөгдлийг зураг болгон дүрслэх нь ердийн тооноос харагдахгүй чиг хандлага, огцом өсөлт болон гажгуудыг ил гаргадаг.
Өгөгдлийг зурахад бэлэн болгох: Алдаа засахдаа print эсвэл лог бичих тушаал
нэмэхдээ өгөгдлийг дараа нь хялбар график болгох форматтай бичихийг анхаараарай.
CSV форматтай цагийн тэмдэг болон утга (1705012345,42.5) нь урт өгүүлбэрээс хамаагүй
хялбар зурагддаг. JSON бүтэцтэй логуудыг мөн багахан хүчин чармайлтаар шинжилж
зураглаж болно. Өөрөөр хэлбэл, өгөгдлөө цэгцтэй хэлбэрээр
лог болгоорой.
Gnuplot-оор хурдан зурах: Командын мөрнөөс хурдан график зурахын тулд
gnuplot хэрэгслийг ашиглан өгөгдлийн файлаас шууд
зураг үүсгэж болно:
# Цагийн тэмдэг, утга бүхий энгийн CSV өгөгдлийг зурах
gnuplot -e "set datafile separator ','; plot 'latency.csv' using 1:2 with lines"
Matplotlib болон ggplot2-оор гүнзгийрүүлэн судлах: Илүү нарийн шинжилгээний
хувьд Python-ий matplotlib болон R-ийн ggplot2
нь өгөгдлийг интерактив байдлаар судлах боломжийг олгодог. Нэг удаагийн зураглалаас
ялгаатай нь эдгээр хэрэгслүүд өгөгдлийг хурдан хэсэгчлэн авч, таамаглалаа шалгах
боломжийг олгоно. ggplot2-ийн facet plots нь маш хүчирхэг — та нэг өгөгдлийг
ангиллаар нь (жишээ нь, хүсэлтийн хоцролтыг endpoint эсвэл өдрийн цагаар салгах)
олон жижиг график болгон хувааж, нуугдмал байсан зүй тогтлыг ил гаргаж чадна.
Ашиглах жишээнүүд:
- Хүсэлтийн хугацааг цаг хугацааны явцад зурахад үе үе гардаг удаашралууд (garbage collection, cron ажлууд, хандалтын урсгал) илэрдэг бөгөөд энэ нь ердийн дундаж тооноос харагддаггүй
- Өсөн нэмэгдэж буй өгөгдлийн бүтцэд элемент нэмэх хугацааг дүрслэх нь алгоритмын нарийн төвөгтэй байдлын асуудлыг ил гаргадаг — динамик массивын (vector) хэмжээ хоёр дахин томрох үед гардаг огцом өсөлтийг графикаас тодорхой харж болно
- Өгөгдлийг өөр өөр хэмжигдэхүүнээр (хүсэлтийн төрөл, хэрэглэгчийн бүлэг, сервер) салгаж дүрслэх нь “системийн хэмжээний” асуудал үнэндээ зөвхөн нэг ангилалд хамаатай байгааг илрүүлдэг
CPU Profiler-үүд
Хүмүүс profilers (хурд хэмжигч) гэж ярихдаа ихэвчлэн CPU profilers-ийг хэлдэг. Үндсэн хоёр төрөл байдаг:
- Tracing profiler-үүд нь програмын хийсэн функцын дуудлага бүрийг бүртгэж хадгалдаг
- Sampling profiler-үүд нь програмын ажиллагааг үе үе (ихэвчлэн миллисекунд тутамд) шалгаж, кодын stack-ийг тэмдэглэж авдаг
Sampling profiler-үүд нь ажиллагааны overhead багатай байдаг тул практикт илүү өргөн хэрэглэгддэг.
perf: Sampling Profiler
perf нь Linux-ийн
стандарт profiler хэрэгсэл юм. Энэ нь кодыг дахин хөрвүүлэх шаардлагагүйгээр
ямар ч програмыг хэмжиж чадна:
perf stat нь хаана цаг зарцуулагдаж байгааг товч харуулна:
$ perf stat ./slow_program
Performance counter stats for './slow_program':
3,210.45 msec task-clock # 0.998 CPUs utilized
12 context-switches # 3.738 /sec
0 cpu-migrations # 0.000 /sec
156 page-faults # 48.587 /sec
12,345,678,901 cycles # 3.845 GHz
9,876,543,210 instructions # 0.80 insn per cycle
1,234,567,890 branches # 384.532 M/sec
12,345,678 branch-misses # 1.00% of all branches
Бодит програмуудын хувьд profiler-ийн гаралт нь асар их хэмжээний мэдээлэл агуулдаг. Хүмүүс визуал дүрслэлд илүү сайн байдаг тул асар их тоон жагсаалтыг унших нь хэцүү байдаг. Flame graphs (Дөлөн график) нь хэмжилтийн өгөгдлийг ойлгоход маш хялбар болгодог дүрслэл юм.
Flame graph нь Y тэнхлэгийн дагуу функцын дуудлагуудын шатлалыг харуулах ба X тэнхлэгийн дагуу зарцуулсан хугацааг харуулдаг. Тэд интерактив байдаг — та тодорхой хэсэг дээр дарж томруулан үзэж болно.
perf өгөгдлөөс flame graph үүсгэхийн тулд:
# Хэмжилтийг бичиж авах
perf record -g ./my_program
# Flame graph үүсгэх (flamegraph скриптүүд хэрэгтэй)
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
Интерактив вэб хувилбараар харахын тулд Speedscope хэрэгслийг ашиглаж болох ба системийн түвшний гүнзгий дүн шинжилгээнд Perfetto-ийг ашиглаж болно.
Valgrind-ийн Callgrind: Tracing Profiler
callgrind нь таны програмын
дуудлагын түүх (call history) болон тушаалуудын тоог бүртгэдэг profiler хэрэгсэл юм.
Sampling profiler-оос ялгаатай нь энэ нь дуудлагын яг таг тоог гаргаж, дуудаж
буй болон дуудагдаж буй функцуудын хамаарлыг харуулдаг:
# callgrind-тэй ажиллуулах
valgrind --tool=callgrind ./my_program
# Үр дүнг callgrind_annotate (текст) эсвэл kcachegrind (GUI) ашиглан шинжлэх
callgrind_annotate callgrind.out.<pid>
kcachegrind callgrind.out.<pid>
Callgrind нь sampling profiler-оос удаан ажилладаг боловч нарийн тоог харуулж
чаддаг бөгөөд хэрэв хүсвэл кэшийн ажиллагааг дуурайлган шалгаж (--cache-sim=yes)
болдог.
Хэрэв та тодорхой програмчлалын хэл ашиглаж байгаа бол илүү тусгай хэрэгслүүд байдаг. Жишээ нь, Python-д
cProfileболонpy-spy, Go-дgo tool pprof, Rust-дcargo-flamegraph(үнэндээ ямар ч хөрвүүлэгддэг хэлний програм дээр ажиллана!) байдаг.
Memory Profiler-үүд
Memory profiler-үүд нь програмын санах ойн хэрэглээг цаг хугацааны явцад ойлгож, санах ойн алдагдлыг олоход тусалдаг.
Valgrind’s Massif
massif нь heap санах ойн
хэрэглээг хэмждэг:
valgrind --tool=massif ./my_program
ms_print massif.out.<pid>
Энэ нь heap санах ойн хэрэглээг цаг хугацааны явцад харуулж, санах ойн алдагдал болон хэт их нөөц хуваарилалтыг илрүүлэхэд тусалдаг.
Python-ийн хувьд
memory-profilerнь мөр мөрөөр санах ойн хэрэглээг харуулдаг.
Benchmarking
Өөр өөр хэрэгжүүлэлт эсвэл хэрэгслүүдийн хурдыг харьцуулах шаардлагатай үед
командын мөрийн програмуудыг харьцуулахад hyperfine
маш тохиромжтой байдаг:
$ hyperfine --warmup 3 'fd -e jpg' 'find . -iname "*.jpg"'
Benchmark #1: fd -e jpg
Time (mean ± σ): 51.4 ms ± 2.9 ms [User: 121.0 ms, System: 160.5 ms]
Range (min … max): 44.2 ms … 60.1 ms 56 runs
Benchmark #2: find . -iname "*.jpg"
Time (mean ± σ): 1.126 s ± 0.101 s [User: 141.1 ms, System: 956.1 ms]
Range (min … max): 0.975 s … 1.287 s 10 runs
Summary
'fd -e jpg' ran
21.89 ± 2.33 times faster than 'find . -iname "*.jpg"'
Вэб хөгжүүлэлтийн хувьд хөтчийн хөгжүүлэгчийн хэрэгсэлд маш сайн profiler-ууд багтсан байдаг. Firefox Profiler болон Chrome DevTools баримт бичгийг үзнэ үү.
Exercises
Debugging
-
Эрэмбэлэх алгоритмын алдааг засах: Дараах псевдо код нь merge sort алгоритмыг хэрэгжүүлсэн боловч алдаа агуулж байна. Үүнийг өөрийн сонгосон хэл дээр бичээд, дараа нь debugger (gdb, lldb, pdb, эсвэл өөрийн IDE-ийн debugger) ашиглан алдааг олж засаарай.
function merge_sort(arr): if length(arr) <= 1: return arr mid = length(arr) / 2 left = merge_sort(arr[0..mid]) right = merge_sort(arr[mid..end]) return merge(left, right) function merge(left, right): result = [] i = 0, j = 0 while i < length(left) AND j < length(right): if left[i] <= right[j]: append result, left[i] i = i + 1 else: append result, right[i] j = j + 1 append remaining elements from left and right return resultТестийн вектор:
merge_sort([3, 1, 4, 1, 5, 9, 2, 6])нь[1, 1, 2, 3, 4, 5, 6, 9]буцаах ёстой. Breakpoint-ууд тавьж, merge функцээр алхам алхмаар орж, хаана буруу элемент сонгогдож байгааг олоорой. -
rr-ийг суулгаж, reverse debugging ашиглан санах ой эвдрэх (corruption) алдааг олно уу. Энэ програмыгcorruption.cнэрээр хадгална уу:#include <stdio.h> typedef struct { int id; int scores[3]; } Student; Student students[2]; void init() { students[0].id = 1001; students[0].scores[0] = 85; students[0].scores[1] = 92; students[0].scores[2] = 78; students[1].id = 1002; students[1].scores[0] = 90; students[1].scores[1] = 88; students[1].scores[2] = 95; } void curve_scores(int student_idx, int curve) { for (int i = 0; i < 4; i++) { students[student_idx].scores[i] += curve; } } int main() { init(); printf("=== Initial state ===\n"); printf("Student 0: id=%d\n", students[0].id); printf("Student 1: id=%d\n", students[1].id); curve_scores(0, 5); printf("\n=== After curving ===\n"); printf("Student 0: id=%d\n", students[0].id); printf("Student 1: id=%d\n", students[1].id); if (students[1].id != 1002) { printf("\nERROR: Student 1's ID was corrupted! Expected 1002, got %d\n", students[1].id); return 1; } return 0; }gcc -g corruption.c -o corruptionгэж хөрвүүлээд ажиллуулна уу. Student 1-ийн ID эвдэрч байгаа боловч энэ нь зөвхөн student 0-ийн утгыг өөрчилдөг функц дотор тохиолдож байна.rr record ./corruptionболонrr replayажиллуулан алдааг олно уу.students[1].idдээр watchpoint тавиад, утга эвдэрсний дарааreverse-continueажиллуулж кодын аль мөр түүнийг дарж бичсэнийг яг таг олно уу. -
AddressSanitizer ашиглан санах ойн алдааг засаарай. Үүнийг
uaf.cнэрээр хадгална уу:#include <stdlib.h> #include <string.h> #include <stdio.h> int main() { char *greeting = malloc(32); strcpy(greeting, "Hello, world!"); printf("%s\n", greeting); free(greeting); greeting[0] = 'J'; printf("%s\n", greeting); return 0; }Эхлээд sanitizer-гүй хөрвүүлж ажиллуулна уу:
gcc uaf.c -o uaf && ./uaf. Энэ нь хэвийн ажиллаж байгаа мэт харагдаж магадгүй. Одоо AddressSanitizer ашиглан хөрвүүлнэ үү:gcc -fsanitize=address -g uaf.c -o uaf && ./uaf. Алдааны тайланг уншина уу. ASan ямар алдаа олсон бэ? Түүний заасан асуудлыг засаарай. -
strace(Linux) эсвэлdtruss(macOS) ашигланls -lкомандын хийж буй систем дуудлагуудыг хянана уу. Ямар систем дуудлагууд хийгдэж байна вэ? Арай илүү төвөгтэй програм хянаж, ямар файлуудыг нээж байгааг хараарай. -
Ойлгомжгүй алдааны мэдээллийг засахад LLM ашиглаж үзээрэй. Хөрвүүлэгчийн алдааны мэдээллийг (ялангуяа C++ template эсвэл Rust-ийн алдааг) хуулж тавиад тайлбар болон засах кодыг асууна уу. Мөн
straceэсвэл AddressSanitizer-ийн гаралтыг оруулан асууж үзээрэй.
Profiling
-
perf statажиллуулан өөрийн сонгосон програмын гүйцэтгэлийн үндсэн статистик үзүүлэлтүүдийг хэмжинэ үү. Янз бүрийн тоолуурууд ямар утгатай вэ? -
perf recordашиглан хурдыг хэмжинэ үү. Үүнийгslow.cнэрээр хадгална уу:#include <math.h> #include <stdio.h> double slow_computation(int n) { double result = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < 1000; j++) { result += sin(i * j) * cos(i + j); } } return result; } int main() { double r = 0; for (int i = 0; i < 100; i++) { r += slow_computation(1000); } printf("Result: %f\n", r); return 0; }Дебаг тэмдэгтүүдтэй хөрвүүлнэ үү:
gcc -g -O2 slow.c -o slow -lm.perf record -g ./slowажиллуулж бичээд, дараа ньperf reportажиллуулж хаана цаг зарцуулагдаж байгааг харна уу. Flamegraph скриптүүдийг ашиглан дөлөн график (flame graph) үүсгэж үзээрэй. -
hyperfineашиглан ижил даалгаврыг гүйцэтгэдэг хоёр өөр хэрэгжүүлэлтийг харьцуулж хэмжинэ үү (жишээ нь:findболонfd,grepболонripgrep, эсвэл өөрийн кодын хоёр өөр хувилбар). -
Системд нөөц их шаарддаг програм ажиллаж байх явцад
htopашиглан хянана уу. Процессын ашиглах CPU-ийг хязгаарлахын тулдtaskset-ийг ашиглаж үзээрэй:taskset --cpu-list 0,2 stress -c 3. Яагаадstressгурван CPU ашиглаж чадахгүй байна вэ? -
Түгээмэл тулгардаг асуудал бол ашиглахыг хүссэн сүлжээний портыг өөр нэг процесс аль хэдийн ашиглаж байх явдал юм. Үүнийг хэрхэн олохыг сурна уу: Эхлээд
python -m http.server 4444ажиллуулан 4444 порт дээр вэб сервер эхлүүлнэ үү. Өөр терминал дээрss -tlnp | grep 4444ажиллуулан уг процессыг олно уу. Процессыгkill <PID>ажиллуулан зогсооно уу.
Лиценз: CC BY-NC-SA.