Debugging ба Profiling

Програмчлалын алтан дүрэм бол код нь таны бодсон шиг биш, харин таны хэлсэн шиг ажилладагт оршино. Тэрхүү зөрүүг арилгах нь заримдаа нэлээд хэцүү ажил байж болно. Энэ лекцээр бид алдаатай (buggy) болон нөөц их шаарддаг кодуудтай ажиллах хүчин төгөлдөр арга техникүүд болох дебаг хийх (debugging) болон профилинг (profiling)-ийг үзэх болно.

Debugging

Printf Debugging болон Logging

“Алдааг засах хамгийн үр дүнтэй хэрэгсэл бол анхааралтай тунгаан бодох болон зөв газарт нь тавьсан print тушаалууд хэвээр байна.” — Brian Kernighan, Unix for Beginners.

Програмыг дебаг хийх анхны бөгөөд энгийн арга бол асуудал илэрсэн хэсгийн орчимд print тушаалуудыг (print statements) нэмж, асуудал юунаас болж байгааг ойлгох хангалттай мэдээлэл цуглуулах хүртлээ дахин дахин ажиллуулж шалгах явдал юм.

Хоёр дахь арга бол кодондоо дурын print тушаал нэмэхийн оронд лог хөтлөх (logging) явдал юм. Лог хөтлөх нь үндсэндээ “илүү анхааралтай хэвлэх” үйлдэл бөгөөд ихэвчлэн дараах бэлэн боломжууд бүхий лог сангуудыг (logging framework) ашигладаг:

Програм бичих явцдаа урьдчилан лог бичих тушаалуудыг тавьж өгснөөр дебаг хийхэд хэрэгтэй өгөгдөл бэлэн байх давуу талтай! Мөн print тушаал ашиглан асуудлыг олоод зассаныхаа дараа тэдгээрийг устгахын оронд зөв лог тушаал болгон үлдээх нь зүйтэй. Ингэснээр ирээдүйд ижил алдаа гарахад кодыг өөрчлөх шаардлагагүйгээр оношилгооны мэдээлэл бэлэн байх болно.

Гуравдагч талын логууд (Third-party logs): Маш олон програмууд ажиллахдаа илүү дэлгэрэнгүй мэдээлэл хэвлэх -v эсвэл --verbose тугуудыг дэмждэг. Энэ нь тушаал яагаад алдаа заасныг илрүүлэхэд маш хэрэгтэй байдаг. Зарим нь бүр илүү дэлгэрэнгүй мэдээлэл авахын тулд тугийг давтан ашиглахыг зөвшөөрдөг. Үйлчилгээнүүдийн (өгөгдлийн сан, вэб сервер гэх мэт) алдааг засахдаа тэдгээрийн логуудыг шалгаарай — Linux дээр ихэвчлэн /var/log/ хавтаст байдаг. Systemd үйлчилгээнүүдийн логийг харахдаа journalctl -u <service> ашиглана уу. Гуравдагч талын сангуудын (libraries) хувьд орчны хувьсагч (environment variables) эсвэл тохиргоогоор дамжуулан дебаг логийг (debug logging) идэвхжүүлэх боломжтой эсэхийг шалгаарай.

Debugger-үүд

Хэвлэж дебаг хийх (print debugging) нь юуг хэвлэхээ мэдэж байгаа болон кодыг амархан өөрчилж дахин ажиллуулах боломжтой үед сайн тохирдог. Харин ямар мэдээлэл хэрэгтэй байгаагаа мэдэхгүй байх, алдаа нь дахин гаргахад маш хэцүү нөхцөлд илэрдэг, эсвэл кодыг өөрчилж програмыг дахин эхлүүлэх нь маш өндөр өртөгтэй (ачаалах хугацаа урт, төлөвийг дахин үүсгэхэд нарийн төвөгтэй гэх мэт) үед debugger-үүд маш чухал үүрэг гүйцэтгэдэг.

Debugger нь програмын ажиллагааг шууд хянах боломжийг олгодог програм бөгөөд дараах үйлдлүүдийг хийж чадна:

Ихэнх програмчлалын хэлнүүд 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 тушаал:

Командын мөрийн хажуугаар эх кодыг хамт харахын тулд 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) тушаалуудыг ашиглаж болно:

Энэ нь дебаг хийхэд үнэхээр хүчирхэг боломж юм. Програм гацсан үед хаана алдаа гарсныг тааж breakpoint тавихын оронд та:

  1. Гацах цэг хүртэл ажиллуулна
  2. Эвдэрсэн төлөвийг шалгана
  3. Эвдэрсэн хувьсагч дээр watchpoint тавина
  4. reverse-continue ажиллуулж хаана уг хувьсагчийн утга буруу өөрчлөгдсөнийг (corrupted) яг таг олно

rr-ийг хэзээ ашиглах вэ:

Тэмдэглэл: 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) нь дараах алдаануудыг илрүүлдэг:

# AddressSanitizer-тай хөрвүүлэх
gcc -fsanitize=address -g program.c -o program
./program

Дараах хэрэгцээт sanitizer-ууд байдаг:

Sanitizer-ууд нь кодыг дахин хөрвүүлэхийг (recompilation) шаарддаг боловч CI систем болон өдөр тутмын хөгжүүлэлтийн явцад ажиллуулахад хангалттай хурдан байдаг.

Valgrind: Дахин хөрвүүлэх боломжгүй үед

Valgrind нь санах ойн алдааг олохын тулд таны програмыг виртуал машин шиг орчинд ажиллуулдаг. Энэ нь sanitizer-уудаас удаан боловч дахин хөрвүүлэхийг шаарддаггүй:

valgrind --leak-check=full ./my_program

Valgrind-ийг хэзээ ашиглах вэ:

Valgrind нь үнэндээ маш хүчирхэг хяналттай ажиллуулах орчин бөгөөд бид үүнийг профилинг хэсэгт дахин харах болно!

AI-ийг Debugging-д ашиглах

Том хэмжээний хэлний загварууд (LLM) нь дебаг хийхэд гайхалтай хэрэгтэй туслагч болж байна. Тэд уламжлалт хэрэгслүүдийг нөхөх зарим дебаг хийх үйлдлүүдэд маш сайн байдаг.

LLM-үүдийн сайн хийж чаддаг зүйлс:

Дебаг тэмдэгтүүд оруулах тухай (Debug symbols): Хэрэгцээтэй stack trace болон дебаг хийхийн тулд өөрийн binary файлыг (болон холбогдох сангуудыг) дебаг тэмдэгтүүдтэй (-g туг) хөрвүүлэх шаардлагатай. Дебаг мэдээлэл ихэвчлэн DWARF форматыг ашигладаг. Үүнээс гадна, frame pointers-тэй (-fno-omit-frame-pointer) хөрвүүлэх нь stack trace-ийг, ялангуяа хурд хэмжигч (profiling) хэрэгслүүдийн хувьд илүү найдвартай болгодог. Эдгээр байхгүй тохиолдолд stack trace нь зөвхөн санах ойн хаягуудыг харуулах эсвэл дутуу байх аюултай. Энэ нь Python эсвэл Java-аас илүү хөрвүүлэгддэг хэлнүүдийн хувьд маш чухал юм.

Анхаарах хязгаарлалтууд:

Энэ нь Хөгжүүлэлтийн орчин лекцээр үзсэн ерөнхий AI код бичих боломжоос тусдаа юм. Энд бид зөвхөн LLM-ийг дебаг хийх туслах болгон ашиглах талаар ярьж байна.

Profiling

Таны код функциональ байдлаар зөв ажиллаж байсан ч тэрээр процессор (CPU) болон санах ойг бүхэлд нь ашиглаад байвал хангалтгүй юм. Алгоритмын хичээлээр ихэвчлэн том O тэмдэглэгээг заадаг боловч програмын хамгийн их ачаалалтай хэсгийг (hot spots) хэрхэн олохыг заадаггүй. Хэт эрт оновчтой болгох нь бүх гай гамшгийн үндэс тул та profiler болон хянах хэрэгслүүдийг сурах хэрэгтэй. Тэд танд програмын аль хэсэг хамгийн их цаг болон нөөцийг зарцуулж байгааг ойлгоход туслах бөгөөд ингэснээр та зөвхөн тэр хэсгийг оновчтой болгоход анхаарлаа хандуулах боломжтой болно.

Хугацаа хэмжих

Гүйцэтгэлийг хэмжих хамгийн энгийн арга бол хугацааг хэмжих явдал юм. Олон нөхцөлд кодын хоёр цэгийн хооронд цагийг хэвлэхэд л хангалттай байдаг.

Гэвч wall clock цаг (бодит цаг) нь төөрөгдөл үүсгэж болно, учир нь компьютер тань нэгэн зэрэг өөр процессуудыг ажиллуулж байгаа эсвэл сүлжээний хариу хүлээж байж болно. time команд нь Real (Бодит), User (Хэрэглэгчийн), болон Sys (Системийн) цагийг ялгаж харуулдаг:

$ time curl https://missing.csail.mit.edu &> /dev/null
real	0m0.272s
user	0m0.079s
sys	    0m0.028s

Энд хүсэлт нь нийт 300 орчим миллисекунд үргэлжилсэн боловч CPU-ийн цаг нь ердөө 107ms байна (user + sys). Үлдсэн хугацаа нь сүлжээг хүлээхэд зарцуулагдсан байна.

Нөөцийг хянах

Заримдаа програмынхаа гүйцэтгэлийг шинжлэх эхний алхам бол түүний бодит нөөцийн хэрэглээг ойлгох явдал байдаг. Програмууд нөөцийн дутагдалд орох үедээ ихэвчлэн удаан ажилладаг.

Гүйцэтгэлийн өгөгдлийг дүрслэх

Хүмүүс тооны хүснэгтээс илүү график дээрх зүй тогтлыг маш хурдан олж хардаг. Гүйцэтгэлийг шинжлэх үед өгөгдлийг зураг болгон дүрслэх нь ердийн тооноос харагдахгүй чиг хандлага, огцом өсөлт болон гажгуудыг ил гаргадаг.

Өгөгдлийг зурахад бэлэн болгох: Алдаа засахдаа 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 эсвэл өдрийн цагаар салгах) олон жижиг график болгон хувааж, нуугдмал байсан зүй тогтлыг ил гаргаж чадна.

Ашиглах жишээнүүд:

CPU Profiler-үүд

Хүмүүс profilers (хурд хэмжигч) гэж ярихдаа ихэвчлэн CPU profilers-ийг хэлдэг. Үндсэн хоёр төрөл байдаг:

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 тэнхлэгийн дагуу зарцуулсан хугацааг харуулдаг. Тэд интерактив байдаг — та тодорхой хэсэг дээр дарж томруулан үзэж болно.

FlameGraph

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

  1. Эрэмбэлэх алгоритмын алдааг засах: Дараах псевдо код нь 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 функцээр алхам алхмаар орж, хаана буруу элемент сонгогдож байгааг олоорой.

  2. 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 ажиллуулж кодын аль мөр түүнийг дарж бичсэнийг яг таг олно уу.

  3. 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 ямар алдаа олсон бэ? Түүний заасан асуудлыг засаарай.

  4. strace (Linux) эсвэл dtruss (macOS) ашиглан ls -l командын хийж буй систем дуудлагуудыг хянана уу. Ямар систем дуудлагууд хийгдэж байна вэ? Арай илүү төвөгтэй програм хянаж, ямар файлуудыг нээж байгааг хараарай.

  5. Ойлгомжгүй алдааны мэдээллийг засахад LLM ашиглаж үзээрэй. Хөрвүүлэгчийн алдааны мэдээллийг (ялангуяа C++ template эсвэл Rust-ийн алдааг) хуулж тавиад тайлбар болон засах кодыг асууна уу. Мөн strace эсвэл AddressSanitizer-ийн гаралтыг оруулан асууж үзээрэй.

Profiling

  1. perf stat ажиллуулан өөрийн сонгосон програмын гүйцэтгэлийн үндсэн статистик үзүүлэлтүүдийг хэмжинэ үү. Янз бүрийн тоолуурууд ямар утгатай вэ?

  2. 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) үүсгэж үзээрэй.

  3. hyperfine ашиглан ижил даалгаврыг гүйцэтгэдэг хоёр өөр хэрэгжүүлэлтийг харьцуулж хэмжинэ үү (жишээ нь: find болон fd, grep болон ripgrep, эсвэл өөрийн кодын хоёр өөр хувилбар).

  4. Системд нөөц их шаарддаг програм ажиллаж байх явцад htop ашиглан хянана уу. Процессын ашиглах CPU-ийг хязгаарлахын тулд taskset-ийг ашиглаж үзээрэй: taskset --cpu-list 0,2 stress -c 3. Яагаад stress гурван CPU ашиглаж чадахгүй байна вэ?

  5. Түгээмэл тулгардаг асуудал бол ашиглахыг хүссэн сүлжээний портыг өөр нэг процесс аль хэдийн ашиглаж байх явдал юм. Үүнийг хэрхэн олохыг сурна уу: Эхлээд python -m http.server 4444 ажиллуулан 4444 порт дээр вэб сервер эхлүүлнэ үү. Өөр терминал дээр ss -tlnp | grep 4444 ажиллуулан уг процессыг олно уу. Процессыг kill <PID> ажиллуулан зогсооно уу.


Энэ хуудсыг засварлах.

Лиценз: CC BY-NC-SA.