Debugging ба Profiling

Програмчлалын алтан дүрэм: код таны бодсон шиг биш, таны бичсэн шиг ажилладаг. Тэр хоёрын зөрүүг олох нь заримдаа хамгийн хэцүү ажил болдог. Энэ лекцээр buggy кодыг debug хийх, мөн удаан эсвэл resource их иддэг кодыг profiling хийж ойлгох аргуудыг үзнэ.

Debugging

Printf Debugging болон Logging

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

Debug хийх хамгийн эхний, хамгийн энгийн арга бол асуудалтай хэсгийн ойролцоо print statement нэмээд, юу болоод байгааг ойлгох хүртлээ дахин ажиллуулж харах.

Дараагийн шат нь хаа сайгүй random print хийхийн оронд logging ашиглах. Logging гэдэг нь үндсэндээ “сайн зохион байгуулалттай print” бөгөөд ихэвчлэн logging framework-ийн дараах боломжууд дээр тулгуурладаг:

Код бичиж байхдаа зөв газарт нь log тавьчихвал дараа debug хийхэд хэрэгтэй өгөгдөл аль хэдийн бэлэн байна. Print-ээр асуудлаа олоод зассаны дараа бүгдийг нь устгахын оронд хэрэгтэйг нь log болгож үлдээвэл ирээдүйд ижил асуудал гарахад кодоо дахин өөрчлөхгүйгээр оношилж чадна.

Гуравдагч талын логууд (Third-party logs): Маш олон програмууд ажиллахдаа илүү дэлгэрэнгүй мэдээлэл хэвлэх -v эсвэл --verbose flag-уудыг дэмждэг. Энэ нь command яагаад алдаа заасныг илрүүлэхэд их хэрэг болдог. Зарим нь бүр илүү дэлгэрэнгүй мэдээлэл авахын тулд тугийг давтан ашиглахыг зөвшөөрдөг. Үйлчилгээнүүдийн (өгөгдлийн сан, вэб сервер гэх мэт) алдааг засахдаа тэдгээрийн логуудыг шалгаарай — Linux дээр ихэвчлэн /var/log/ folder-т байдаг. 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 command:

Командын мөрийн хажуугаар эх кодыг хамт харахын тулд GDB-ийн TUI горимыг (gdb -tui эсвэл GDB дотор Ctrl-x a дарах) ашиглаж үзээрэй.

Record-Replay Debugging

Хамгийн ядаргаатай алдаануудын нэг бол Heisenbugs буюу тэдгээрийг ажиглахыг оролдох үед алга болдог эсвэл зангаа хувиргадаг алдаанууд юм. Уралдааны нөхцөл (race conditions), хугацаанаас хамаарсан алдаанууд болон зөвхөн системийн тодорхой нөхцөлд гардаг алдаанууд үүнд багтана. Програмыг дахин ажиллуулахад өөр үр дүн гардаг тул (жишээ нь, print statement нэмэх нь кодын ажиллагааг хангалттай удаашруулж, уралдааны нөхцөл үүсэхээ болих) уламжлалт аргаар засах нь үр дүнгүй байдаг.

Record-replay debugging нь програмын ажиллагааг бичиж аваад, түүнийг хэдэн ч удаа яг ижилхэн, детерминистик (deterministic) байдлаар дахин ажиллуулах боломжийг олгосноор үүнийг шийддэг. Тэр ч байтугай, асуудал хаана гарсныг олохын тулд ажиллагааг ухрааж (reverse) үзэж болно.

rr нь Linux-д зориулсан хүчтэй хэрэгсэл бөгөөд програмын ажиллагааг бичиж аваад, бүрэн дебаг хийх боломжтойгоор детерминистик байдлаар дахин ажиллуулдаг. Энэ нь GDB-тэй хамтарч ажилладаг тул та интерфейсийг нь аль хэдийн мэднэ.

Үндсэн хэрэглээ:

# Record a program execution
rr record ./my_program

# Replay the recording (opens GDB)
rr replay

Шидтэй хэсэг нь дахин ажиллуулах явцад болдог. Ажиллагаа нь детерминистик тул та reverse debugging command-уудыг ашиглаж болно:

Энэ нь дебаг хийхэд үнэхээр хэрэгтэй боломж. Програм гацсан үед хаана алдаа гарсныг тааж 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 нь хамгийн шилдэг нь юм: та ажиллагааг яг таг дахин үүсгэж, reverse debugging command-аар санах ойн эвдрэлийг (corruption) мөшгөх боломжтой.

System Call Tracing

Заримдаа програм тань үйлдлийн системтэй хэрхэн харилцаж байгааг ойлгох шаардлагатай болдог. Програмууд нь цөмөөс (kernel) үйлчилгээ авахын тулд систем дуудлага (system calls) хийдэг — файл нээх, санах ой хуваарилах, процесс үүсгэх гэх мэт. Эдгээр дуудлагыг мөрдөх нь програм яагаад гацсан (hanging) байгаа, ямар файл руу хандахыг оролдож байгаа, эсвэл хаана хүлээж цаг алдаж байгааг харуулж чаддаг.

strace (Linux) болон dtruss (macOS)

strace нь програмын хийж буй систем дуудлага бүрийг ажиглах боломжийг олгодог:

# Trace all system calls
strace ./my_program

# Trace only file-related calls
strace -e trace=file ./my_program

# Follow child processes (important for programs that start other programs)
strace -f ./my_program

# Trace a running process
strace -p <PID>

# Show timing information
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 програм бичих өндөр түвшний syntax санал болгодог. Эдгээр нь kernel дотор ажилладаг тул overhead бага, system call-уудыг тоолох, хугацаа хэмжих, аргумент шалгах зэрэгт ашиглагддаг.

# Trace file opens system-wide (prints immediately)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'

# Count system calls by name (prints summary on Ctrl-C)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'

Та мөн bcc хэрэгслийг ашиглан C хэл дээр eBPF програмуудыг шууд бичиж болох бөгөөд үүнд дискний үйлдлийн хоцролтыг хэмждэг biosnoop, нээгдсэн файлуудыг хэвлэдэг opensnoop зэрэг олон бэлэн хэрэгслүүд багтсан байдаг.

strace нь хурдан ажиллуулж эхлэхэд хялбар байдгаараа хэрэгцээтэй байдаг бол bpftrace нь overhead багатай ажиллах, цөмийн функцуудыг хянах, нэгтгэсэн тооцоолол хийх шаардлагатай үед илүү тохиромжтой. bpftrace-ийг ажиллуулахад root эрх хэрэгтэй бөгөөд тэрээр тодорхой процесс биш, харин системийн хэмжээнд цөмийг хянадаг. Тодорхой програмыг хянахын тулд command-ын нэр эсвэл PID-ээр шүүж болно:

# Filter by command name (prints summary on Ctrl-C)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /comm == "bash"/ { @[probe] = count(); }'

# Trace a specific command from startup using -c (cpid = child PID)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* /pid == cpid/ { @[probe] = count(); }' -c 'ls -la'

-c flag нь заасан command-ыг ажиллуулаад cpid-ийг түүний PID-ээр оноодог. Ингэснээр програмыг эхлэх мөчөөс нь trace хийхэд эвтэйхэн. Програм дуусахад bpftrace нэгтгэсэн үр дүнг хэвлэнэ.

Network Debugging

Сүлжээний асуудлуудын хувьд tcpdump болон Wireshark нь сүлжээний пакетуудыг барьж аваад шинжлэх боломжийг олгодог:

# Capture packets on port 80
sudo tcpdump -i any port 80

# Capture and save to file for Wireshark analysis
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) нь дараах алдаануудыг илрүүлдэг:

# Compile with 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 нь үнэндээ маш хүчтэй instrumentation орчин бөгөөд profiling хэсэгт дахин гарч ирнэ.

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). Үлдсэн хугацаа нь сүлжээг хүлээхэд зарцуулагдсан байна.

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

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

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

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

Өгөгдлийг зурахад бэлэн болгох: Debug хийхдээ print эсвэл log statement нэмэх бол өгөгдлөө дараа нь амархан график болгох форматтай бичээрэй. CSV форматтай цагийн тэмдэг болон утга (1705012345,42.5) нь урт өгүүлбэрээс хамаагүй хялбар зурагддаг. JSON бүтэцтэй логуудыг мөн багахан хүчин чармайлтаар шинжилж зураглаж болно. Өөрөөр хэлбэл, өгөгдлөө цэгцтэй хэлбэрээр лог болгоорой.

Gnuplot-оор хурдан зурах: Командын мөрнөөс хурдан график зурахын тулд gnuplot хэрэгслийг ашиглан өгөгдлийн файлаас шууд зураг үүсгэж болно:

# Plot a simple CSV with timestamp,value
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 үүсгэхийн тулд:

# Record profile
perf record -g ./my_program

# Generate flame graph (requires flamegraph scripts)
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg

Интерактив вэб хувилбараар харахын тулд Speedscope хэрэгслийг ашиглаж болох ба системийн түвшний гүнзгий дүн шинжилгээнд Perfetto-ийг ашиглаж болно.

Valgrind-ийн Callgrind: Tracing Profiler

callgrind нь таны програмын дуудлагын түүх (call history) болон command-уудын тоог бүртгэдэг profiler хэрэгсэл юм. Sampling profiler-оос ялгаатай нь энэ нь дуудлагын яг таг тоог гаргаж, дуудаж буй болон дуудагдаж буй функцуудын хамаарлыг харуулдаг:

# Run with callgrind
valgrind --tool=callgrind ./my_program

# Analyze with callgrind_annotate (text) or 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

Өөр өөр хэрэгжүүлэлт эсвэл хэрэгслүүдийн хурдыг харьцуулах шаардлагатай үед CLI програмуудыг харьцуулахад 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.