☰
《计算机教育中缺失的一课》中文版调试与性能分析实战指南:日志、调试器与性能剖析工具链
2026/9/28 2:37:54 网站建设 项目流程
  • 文档
  • 教程
  • 教育

【免费下载链接】missing-semester-cn.github.io

the CS missing semester Chinese version

项目地址:https://gitcode.com/gh_mirrors/mi/missing-semester-cn.github.io
点击查看免费下载

本篇文章基于本仓库_2020/debugging-profiling.md(2020 年讲座「调试及性能分析」中文版)整理扩展而成,并辅以仓库内 static/files/logger.py 与 static/files/sorts.py 等配套示例源码进行深化。通过本文,你将掌握一套完整的"调试 + 性能分析"方法论:从打印调试与结构化日志、系统日志查询、pdb/gdb 调试器,到静态分析、CPU/内存剖析、火焰图可视化与系统资源监控,可直接套用到日常开发排障与性能优化场景中。

编程界有一条金科玉律:代码不能完全按照你的想法运行,它只能完全按照你的写法运行。让"写法"符合"想法"非常困难,而调试与性能分析正是弥合这一差距的核心技能。本仓库收录的 2020 年讲座《调试及性能分析》系统地传授了两类技术:处理代码中bug的调试技术,和处理程序性能问题(CPU、内存、I/O 资源消耗)的性能分析技术。本文将原讲座内容完整展开,并结合作业配套的示例源码深入讲解每个工具的实战用法。

调试代码

打印调试法与日志

"最有效的 debug 工具就是细致的分析,配合恰当位置的打印语句。" —— Brian Kernighan,《Unix 新手入门》

调试代码的第一种方法,往往是在发现问题的地方添加一些打印语句,然后不断重复此过程,直到获取足够的信息、找到问题的根本原因。这种"打印调试法"(printf debugging)简单直接,在大部分场景下都够用。

第二种方法是使用**日志(logging)**而非临时添加打印语句。日志本质上就是"更讲究的打印",与临时打印相比有如下优势:

  • 你可以将日志写入文件、socket,甚至是发送到远端服务器,而不仅仅输出到标准输出;
  • 日志支持严重等级(例如INFO、DEBUG、WARN、ERROR等),使你可以根据需要过滤日志;
  • 对于新发现的问题,很可能你的日志中已经包含了足以定位问题的信息——因为日志是在编程时就主动埋下的诊断数据。

仓库配套示例:带颜色的日志程序

仓库的 static/files/logger.py 就是讲座中提到的日志示例程序,它演示了标准库logging的完整用法,包括自定义 Formatter 实现彩色输出。核心实现如下:

  • 定义CustomFormatter(logging.Formatter)子类,通过 ANSI 转义序列为不同日志等级着色:DEBUG/INFO用灰色(\x1b[38;21m)、WARNING用黄色(\x1b[33;21m)、ERROR用红色(\x1b[31;21m)、CRITICAL用加粗红色(\x1b[31;1m),统一以\x1b[0m复位;
  • 默认格式串为"%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)",即包含时间、logger 名、等级、消息正文以及所在文件与行号,这为事后定位提供了精确位置;
  • 程序根据命令行参数切换输出模式:log参数改用纯文本格式'%(asctime)s : %(levelname)s : %(name)s : %(message)s',color参数则启用CustomFormatter;第二个参数(如ERROR)通过logging.__getattribute__动态设置 logger 的过滤等级;
  • 主体循环随机生成 0~10 的整数,分别触发info、warning、error、critical日志并time.sleep(0.3),模拟一个持续产生日志的长期运行程序。

在仓库目录下可以直接这样运行它:

$ python static/files/logger.py # Raw output as with just prints $ python static/files/logger.py log # Log formatted output $ python static/files/logger.py log ERROR # Print only ERROR levels and above $ python static/files/logger.py color # Color formatted output

ANSI 转义序列与终端彩色输出

有很多技巧可以提升日志的可读性,最常用的是着色。ls、grep这类程序使用 ANSI escape codes(ANSI 转义序列),它们是一系列特殊字符,可以让 shell 改变输出颜色。例如执行下面的命令会打印红色的字符串This is red:

echo -e "\e[38;2;255;0;0mThis is red\e[0m"

其中\e[38;2;R;G;Bm是 24 位"真彩色"前景色设置,\e[0m复位所有属性。前提是终端支持真彩色(true color);如果不支持(例如 macOS 自带的 Terminal.app),可以使用兼容性更广的 16 色方案,例如:

echo -e "\e[31;1mThis is red\e[0m"

下面的脚本展示了如何在支持真彩色的终端中遍历打印 0、20、40……255 各通道的颜色块:

#!/usr/bin/env bash for R in $(seq 0 20 255); do for G in $(seq 0 20 255); do for B in $(seq 0 20 255); do printf "\e[38;2;${R};${G};${B}m█\e[0m"; done done done

第三方日志与系统日志

如果你正在构建大型软件系统,很可能会用到一些作为独立程序运行的依赖,如 Web 服务器、数据库或消息代理。与这些系统交互时,阅读它们的日志非常必要,因为仅靠客户端侧的错误信息往往不足以定位问题。

日志存放位置

大多数程序会把日志保存在系统中的某个地方。对于 UNIX 系统,程序日志通常存放在/var/log。例如 NGINX Web 服务器将其日志存放于/var/log/nginx。

目前系统开始普遍采用system log(系统日志),所有日志都会汇总到这里:

  • 大多数(但不是全部)Linux 系统使用systemd系统守护进程,它控制系统中的很多东西(例如哪些服务应该启动并运行)。systemd将日志以特殊格式存放于/var/log/journal,可以使用journalctl命令显示这些消息;
  • macOS 上,除了/var/log/system.log,越来越多的工具使用log show显示系统日志;
  • 对于大多数 UNIX 系统,可以使用dmesg命令读取内核日志。

向系统日志写入与检索

如果你希望把自己的日志加入系统日志,可以使用logger这个 shell 程序。大多数编程语言也提供写系统日志的方法。下面这个例子展示了如何写入一条日志并在两个平台上把它检索出来:

logger "Hello Logs" # On macOS log show --last 1m | grep Hello # On Linux journalctl --since "1m ago" | grep Hello

正如在数据整理那节课上看到的,日志内容可以非常多,需要处理和过滤才能得到想要的信息。如果经常需要对journalctl和log show的结果做大量过滤,可以考虑先使用它们自带的选项(如--since、--last)在源头过滤。另外,lnav这类工具为日志文件提供了更好的展现和浏览方式。

调试器

当打印已经不能满足调试需求时,应该使用调试器。调试器是允许我们与正在执行的程序交互的程序,它可以做到:

  • 当到达某一行时将程序暂停;
  • 一次一条指令地逐步执行程序;
  • 程序崩溃后查看变量的值;
  • 满足特定条件时暂停程序;
  • 以及其他高级功能。

Python 的 pdb

很多编程语言都有自己的调试器,Python 的调试器是pdb。下面是pdb支持的核心命令简介:

  • l(ist):显示当前行周围的 11 行,或接着上次显示继续往下 11 行;
  • s(tep):执行当前行,并在第一个可能的时机停止(通常指步入函数);
  • n(ext):继续执行,直到到达当前函数的下一行或函数返回(通常指步过函数);
  • b(reak):设置断点(基于传入的参数);
  • p(rint):在当前上下文对表达式求值并打印结果;还有一个pp命令,它使用pprint美化打印;
  • r(eturn):执行到当前函数返回;
  • q(uit):退出调试器。

让我们用pdb修复下面这段有 bug 的冒泡排序代码(参考讲座视频):

def bubble_sort(arr): n = len(arr) for i in range(n): for j in range(n): if arr[j] > arr[j+1]: arr[j] = arr[j+1] arr[j+1] = arr[j] return arr print(bubble_sort([4, 2, 1, 8, 7, 6]))

这段代码的明显问题包括:内层循环上界应为n - i - 1(否则j+1会越界),以及交换赋值写反了(arr[j] = arr[j+1]把大值覆盖掉了)。你可以启动python -m pdb 文件名.py,用l查看源码、b在指定行设断点、n/s单步跟踪、p打印数组中间状态,逐步观察排序过程找到根因。

因为 Python 是解释型语言,所以可以直接在pdbshell 中执行命令和指令。ipdb是pdb的增强版:它使用 IPython 作为 REPL,开启 tab 补全、语法高亮、更好的回溯和更好的内省,同时保留与pdb模块相同的接口。

底层语言调试器:gdb 与 lldb

对于更底层的编程语言,你可能需要了解gdb(及其改进版pwndbg)和lldb。它们针对类 C 语言调试进行了优化,但也允许你探索几乎任何进程并获取其当前的机器状态,例如寄存器、栈、程序计数器等。常见用法包括在崩溃后查看回溯栈(backtrace)、检查内存内容、附加到运行中的进程等。

专门工具:系统调用追踪与网络分析

即使需要调试的程序是一个二进制的"黑盒",仍然有工具可以帮忙。当程序需要执行只有操作系统内核才能完成的操作时,它必须使用系统调用(system call)。在 Linux 中可以用strace追踪系统调用,在 macOS 和 BSD 中则使用dtrace。dtrace使用起来可能有些别扭,因为它使用自有的D语言,但可以用dtruss这个封装使其具有与strace类似的接口。

下面例子展示了如何用strace或dtruss显示执行ls时对stat系统调用的追踪结果:

# On Linux sudo strace -e lstat ls -l > /dev/null # On macOS sudo dtruss -t lstat64_extended ls -l > /dev/null

有些情况下需要查看网络数据包才能定位问题。tcpdump和 Wireshark 这样的网络数据包分析工具可以帮助获取网络数据包的内容,并基于不同条件过滤。

对于 Web 开发,Chrome/Firefox 的开发者工具非常方便且功能强大:

  • 源码:查看任意站点的 HTML/CSS/JS 源码;
  • 实时修改 HTML、CSS、JS 代码:修改网站的内容、样式和行为用于测试(由此可见网页截图是不可信的);
  • Javascript shell:在 JS REPL 中执行命令;
  • 网络:分析请求的时间线;
  • 存储:查看 Cookies 和本地应用存储。

静态分析

有些问题不需要执行代码就能发现。例如,仔细观察一段代码,就能发现某个循环变量覆盖了某个已存在的变量或函数名;或某个变量在被读取之前并没有被定义。静态分析工具正是针对这类问题的:它把程序源码作为输入,基于规则进行分析并对代码正确性进行推理。

下面这段 Python 代码存在几个问题:首先,循环变量foo覆盖了之前定义的函数foo;最后一行把bar错写成了baz,因此程序完成sleep(一分钟)后执行到这一行时便会崩溃:

import time def foo(): return 42 for foo in range(5): print(foo) bar = 1 bar *= 0.2 time.sleep(60) print(baz)

静态分析工具可以发现此类问题。用pyflakes分析时,会得到与这两处 bug 相关的错误信息;mypy则进行类型检查,会指出bar起初是int、后来变成了float——这些问题都无需运行代码即可发现:

$ pyflakes foobar.py foobar.py:6: redefinition of unused 'foo' from line 3 foobar.py:11: undefined name 'baz' $ mypy foobar.py foobar.py:6: error: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") foobar.py:9: error: Incompatible types in assignment (expression has type "float", variable has type "int") foobar.py:11: error: Name 'baz' is not defined Found 3 errors in 1 file (checked 1 source file)

在Shell 工具和脚本那节课介绍了shellcheck,它是应用于 shell 脚本的同类工具。多数编辑器和 IDE 都支持在编辑器界面内直接显示这些工具的输出,并高亮警告和错误的位置,这通常被称为代码检查(code linting),它也可以用来展示代码风格违规或不安全的代码结构:

  • 在 vim 中,插件ale或syntastic可以帮助做同样的事情;
  • 在 Python 中,pylint和pep8是风格检查工具的典型例子,bandit则专门用来发现常见安全漏洞;
  • 其他语言也有非常详尽的静态分析工具列表(如 Awesome Static Analysis 的Writing一节),代码检查工具(linters)可参考 Awesome Linters。

与风格检查相辅相成的是代码格式化工具(code formatters),例如 Python 的black、Go 语言的gofmt、Rust 的rustfmt,以及 JavaScript、HTML、CSS 的prettier。这些工具会自动把代码格式化为该语言通用的风格规范。标准化的代码格式不仅有助于他人阅读你的代码,也能让你更轻松地阅读他人(同样经过格式化)的代码。

性能分析(Profiling)

即使代码在功能上完全符合预期,如果它在运行过程中耗尽了所有 CPU 或内存资源,那也未必合格。算法课程通常教授大 O 表示法,却很少教你如何找到程序中的热点(hot spots)。鉴于过早优化是万恶之源,你更应该了解性能分析器(profilers)和监控工具——它们会帮助你找到程序中最耗时、最耗资源的部分,从而让你集中精力优化这些特定部分。

计时

与调试类似,多数情况下只需打印代码从一处运行到另一处的时间即可发现问题。下面是一个使用 Pythontime模块的例子:

import time, random n = random.randint(1, 10) * 100 # 获取当前时间 start = time.time() # 做些工作 print("Sleeping for {} ms".format(n)) time.sleep(n/1000) # 比较当前时间和起始时间 print(time.time() - start) # 输出 # Sleeping for 500 ms # 0.5713930130004883

不过,墙上时间(wall clock time)也可能有误导性,因为计算机可能同时在运行其他进程,或者在等待某些事件发生。工具通常会区分三种时间:

  • 真实时间(Real):程序从开始到结束流逝的墙上时间,包括其他进程使用的时间以及阻塞(例如等待 I/O 或网络)的时间;
  • 用户时间(User):CPU 执行用户态代码所花费的时间;
  • 系统时间(Sys):CPU 执行内核态代码所花费的时间。

通常"用户时间 + 系统时间"代表你的进程在 CPU 上实际消耗的时间。例如,试着在一个网络不佳的环境下执行 HTTP 请求,并在命令前加上time:

$ time curl https://missing.csail.mit.edu &> /dev/null real 0m2.561s user 0m0.015s sys 0m0.012s

请求花费了 2 秒多才完成,但进程仅花费了 15 毫秒 CPU 用户时间和 12 毫秒 CPU 内核时间——其余时间都消耗在等待网络上。

CPU 性能分析工具

大多数情况下,当人们提及性能分析工具时,指的是 CPU 性能分析工具。CPU 性能分析工具有两种:

  • 追踪分析器(tracing):记录程序的每一次函数调用;
  • 采样分析器(sampling):周期性地(通常每毫秒)监测程序并记录程序堆栈。

它们使用这些记录生成统计信息,显示程序在哪些事情上花费了最多的时间。大多数编程语言都有基于命令行的分析器,通常可以集成在 IDE 中。

Python 的 cProfile

在 Python 中,使用cProfile模块分析每次函数调用所消耗的时间。下面实现了一个基础的 grep 命令:

#!/usr/bin/env python import sys, re def grep(pattern, file): with open(file, 'r') as f: print(file) for i, line in enumerate(f.readlines()): pattern = re.compile(pattern) match = pattern.search(line) if match is not None: print("{}: {}".format(i, line), end="") if __name__ == '__main__': times = int(sys.argv[1]) pattern = sys.argv[2] for i in range(times): for file in sys.argv[3:]: grep(pattern, file)

用下面的命令对这段代码进行分析。从输出可知:I/O 消耗了大量时间,编译正则表达式也比较耗费时间。因为正则表达式只需编译一次,可以将其移到 for 循环外面来改进性能:

$ python -m cProfile -s tottime grep.py 1000 '^(import|\s*def)[^,]*$' *.py [omitted program output] ncalls tottime percall cumtime percall filename:lineno(function) 8000 0.266 0.000 0.292 0.000 {built-in method io.open} 8000 0.153 0.000 0.894 0.000 grep.py:5(grep) 17000 0.101 0.000 0.101 0.000 {built-in method builtins.print} 8000 0.100 0.000 0.129 0.000 {method 'readlines' of '_io._IOBase' objects} 93000 0.097 0.000 0.111 0.000 re.py:286(_compile) 93000 0.069 0.000 0.069 0.000 {method 'search' of '_sre.SRE_Pattern' objects} 93000 0.030 0.000 0.141 0.000 re.py:231(compile) 17000 0.019 0.000 0.029 0.000 codecs.py:318(decode) 1 0.017 0.017 0.911 0.911 grep.py:3(<module>) [omitted lines]

关于cProfile(以及其他类似的分析器)需要注意:它显示的是每次函数调用的时间,看起来可能反直觉——尤其是使用了第三方函数库时,因为内部函数调用也会被看作函数调用。

行分析器 line_profiler

更符合直觉的显示方式是包含每行代码的执行时间,这正是行分析器的工作。例如,下面这段 Python 代码会向课程网站发起请求,然后解析响应页面中的全部 URL:

#!/usr/bin/env python import requests from bs4 import BeautifulSoup # 这个装饰器会告诉行分析器 # 我们想要分析这个函数 @profile def get_urls(): response = requests.get('https://missing.csail.mit.edu') s = BeautifulSoup(response.content, 'lxml') urls = [] for url in s.find_all('a'): urls.append(url['href']) if __name__ == '__main__': get_urls()

如果用cProfile分析它,会得到超过 2500 行的输出,即使排序也很难搞懂时间花在哪里。改用line_profiler(通过kernprof命令),它会基于行显示时间,一目了然:

$ kernprof -l -v a.py Wrote profile results to urls.py.lprof Timer unit: 1e-06 s Total time: 0.636188 s File: a.py Function: get_urls at line 5 Line # Hits Time Per Hit % Time Line Contents ============================================================== 5 @profile 6 def get_urls(): 7 1 613909.0 613909.0 96.5 response = requests.get('https://missing.csail.mit.edu') 8 1 21559.0 21559.0 3.4 s = BeautifulSoup(response.content, 'lxml') 9 1 2.0 2.0 0.0 urls = [] 10 25 685.0 27.4 0.1 for url in s.find_all('a'): 11 24 33.0 1.4 0.0 urls.append(url['href'])

96.5% 的时间都花在requests.get这一次网络请求上,后续解析和遍历几乎可以忽略。

内存分析

像 C 或 C++ 这样的语言,内存泄漏会导致程序在使用完内存后不去释放它。可以借助 Valgrind 这样的工具检查内存泄漏问题。

对于 Python 这类具有垃圾回收机制的语言,内存分析器同样有用——因为对于某个对象来说,只要有指针还指向它,它就不会被回收。下面这个例子及其输出展示了memory-profiler的工作方式(注意其装饰器与line-profiler类似):

@profile def my_func(): a = [1] * (10 ** 6) b = [2] * (2 * 10 ** 7) del b return a if __name__ == '__main__': my_func()
$ python -m memory_profiler example.py Line # Mem usage Increment Line Contents ============================================== 3 @profile 4 5.97 MB 0.00 MB def my_func(): 5 13.61 MB 7.64 MB a = [1] * (10 ** 6) 6 166.20 MB 152.59 MB b = [2] * (2 * 10 ** 7) 7 13.61 MB -152.59 MB del b 8 13.61 MB 0.00 MB return a

可以看到b = [2] * (2 * 10 ** 7)一次性分配了约 152 MB 内存,del b后立即释放——内存分析器能精确呈现每个分配点的内存增量。

事件分析:perf

在使用strace调试时,你可能希望把某些代码当作黑盒处理。perf命令对 CPU 的差异做了抽象:它不报告时间和内存消耗,而是报告与程序相关的系统事件。例如,perf可以报告不佳的缓存局部性(poor cache locality)、大量的页错误(page faults)或活锁(livelocks)。常用命令简介:

  • perf list:列出可以被 perf 追踪的事件;
  • perf stat COMMAND ARG1 ARG2:收集与某个进程或指令相关的事件;
  • perf record COMMAND ARG1 ARG2:记录命令执行的采样信息,并将统计数据储存在perf.data中;
  • perf report:格式化并打印perf.data中的数据。

可视化

使用分析器分析真实程序时,由于软件复杂性,输出结果包含大量信息。人类是视觉动物,非常不善于阅读大量文字,因此很多工具都提供了可视化分析器输出结果的功能。

对于采样分析器,常见的 CPU 分析数据可视化形式是火焰图(flame graph):它在 Y 轴显示函数调用关系,在 X 轴显示耗时比例。火焰图同时还是可交互的,你可以深入程序的某一具体部分并查看其栈追踪(例如 Brendan Gregg 的 CPU-bash-flamegraph 就是经典示例)。

调用图(call graph)和控制流图可以显示子程序之间的关系:把函数作为节点、函数调用作为边。将它与分析器信息(例如调用次数、耗时等)结合时,调用图会非常有用,可以帮助分析程序流程。在 Python 中可以使用pycallgraph生成这些图片(典型输出如维基百科上由 pycallgraph 生成的调用图示例)。

资源监控

有时候,分析程序性能的第一步是搞清楚它所消耗的资源。程序变慢通常是因为所需资源不够,例如没有足够的内存或网络连接变慢。有很多工具可以显示 CPU 占用、内存使用、网络、磁盘使用等系统资源:

  • 通用监控:最流行的是htop,它是top的改进版,可以显示当前运行进程的多种统计信息。常用快捷键:<F6>进程排序、t显示树状结构、h打开或折叠线程。还可以留意glances,实现类似但用户界面更好;如果需要合并测量全部进程,dstat也很好用,它可以实时计算不同子系统的资源度量数据,例如 I/O、网络、CPU 利用率、上下文切换等;
  • I/O 操作:iotop显示实时 I/O 占用信息,可以方便地检查某个进程是否正在执行大量磁盘读写;
  • 磁盘使用:df显示每个分区的信息,du显示当前目录下每个文件的磁盘使用情况(disk usage),-h选项使输出以对人类更友好的格式显示;ncdu是交互性更好的du,可以在不同目录间导航、删除文件和文件夹;
  • 内存使用:free显示系统当前空闲内存;内存也可以使用htop等工具查看;
  • 打开文件:lsof列出被进程打开的文件信息,当需要查看某个文件被哪个进程打开时非常有用;
  • 网络连接和配置:ss监控网络包的收发情况并显示网络接口信息,常见使用场景是找到占用某端口的进程;查看路由、网络设备和接口信息使用ip命令。注意,netstat和ifconfig已被上述工具取代;
  • 网络使用:nethogs和iftop是很好的交互式命令行网络占用监控工具。

如果想测试这些监控工具,可以使用stress命令为系统人为增加负载。

专用工具:基准测试 hyperfine

有时候,你只需要对黑盒程序进行基准测试,并据此对软件选择进行评估。hyperfine这类命令行工具可以帮你快速完成基准测试。例如,在 Shell 工具和脚本那节课中推荐用fd代替find,这里可以用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"'

在这个示例环境中,fd比find快了约 20 倍(--warmup 3表示先执行 3 次预热再正式测量,避免冷启动和缓存影响)。

和调试一样,浏览器也内置了不错的性能分析工具,可以分析页面加载,让你搞清楚时间消耗在什么地方(加载、渲染、脚本等),例如 Firefox Profiler 与 Chrome DevTools 的性能面板。

课后练习与仓库配套资源

本节练习与配套示例来自原讲座,习题解答的路径由仓库根目录的 _config.yml 配置:solution_url: missing-notes-and-solutions/2020/solutions/,结合本页 front matter 中的solution.url: debugging-profiling-solution,即可定位到对应的习题解答页面。

调试练习

  1. 使用 Linux 上的journalctl或 macOS 上的log show命令,获取最近一天中超级用户的登录信息及其所执行的指令。如果找不到相关信息,可以执行一些无害的命令(例如sudo ls)然后再次查看。

  2. 学习pdb实践教程并熟悉相关命令(例如通过python -m pdb逐步跟踪冒泡排序等小程序的执行,熟悉l/s/n/b/p/r/q命令的配合使用)。

  3. 安装shellcheck并检查下面的脚本。这段代码有什么问题?修复相关问题,并在你的编辑器中安装 linter 插件,让它自动显示警告信息:

    #!/bin/sh ## Example: a typical script with several problems for f in $(ls *.m3u) do grep -qi hq.*mp3 $f \ && echo -e 'Playlist $f contains a HQ file in mp3 format' done

    常见问题包括:for f in $(ls *.m3u)未加引号导致文件名含空格时出错、echo -e中的$f在单引号内不会展开变量、缺少对$f的引用加引号等,shellcheck会逐条指出。

  4. (进阶题)阅读可逆调试(reverse debugging)相关资料,并尝试创建一个可以工作的例子(使用rr或RevPDB)。rr录制程序执行后可确定性重放,配合 GDB 实现"时光倒流"式的反向调试。

性能分析练习

  1. 仓库的 static/files/sorts.py 提供了一些排序算法的实现,包括insertionsort(插入排序)、quicksort(非原地快排)、quicksort_inplace(原地快排),以及一个test_sorted随机测试函数。请使用cProfile和line_profiler比较插入排序和快速排序的性能:

    • 两种算法的瓶颈分别在哪里?(可以从源码推断:quicksort每次递归都会用列表推导式创建left/right新列表,产生大量内存分配;insertionsort是原地元素搬移,但最坏情况下比较/移动次数多;quicksort_inplace通过原地交换减少分配。)
    • 然后使用memory_profiler检查内存消耗:为什么插入排序的内存表现更好?(因为非原地快排在每一层递归都新建列表。)
    • 再看看原地排序版本的快排表现。
    • 附加题:使用perf查看不同算法的循环次数及缓存命中/丢失情况。
  2. 下面这段用于计算斐波那契数列的 Python 代码,为每个数字都定义了一个函数:

    #!/usr/bin/env python def fib0(): return 0 def fib1(): return 1 s = """def fib{}(): return fib{}() + fib{}()""" if __name__ == '__main__': for n in range(2, 10): exec(s.format(n, n-1, n-2)) # from functools import lru_cache # for n in range(10): # exec("fib{} = lru_cache(1)(fib{})".format(n, n)) print(eval("fib9()"))

    将代码拷贝到文件中使其变为可执行程序。首先安装pycallgraph和 graphviz(如果能够执行dot,则说明已安装 GraphViz),然后使用pycallgraph graphviz -- ./fib.py执行代码并查看生成的pycallgraph.png。fib0被调用了多少次?可以通过**记忆化(memoization)**优化:将注释掉的lru_cache部分放开,重新生成图片,这回每个fibN函数被调用了多少次?(不缓存时fib0会被指数级地重复调用;启用lru_cache后每个fibN最多只计算一次。)

  3. 我们经常会遇到"想监听的端口已被其他进程占用"的情况,请通过进程 PID 查找相应进程。首先执行python -m http.server 4444启动一个最简单的 Web 服务器监听4444端口;在另一个终端执行lsof | grep LISTEN打印所有监听端口的进程及对应端口;找到对应 PID 后使用kill <PID>停止该进程。

  4. 限制进程资源也是非常实用的技术。执行stress -c 3并用htop对 CPU 消耗进行可视化;接着执行taskset --cpu-list 0,2 stress -c 3再可视化。stress占用了 3 个 CPU 吗?为什么没有?(taskset把进程绑定到 CPU 0 和 2 两个核心上,stress -c 3只会在可用 CPU 上运行,因此最多使用 2 个 CPU;查阅man taskset寻找答案。)附加题:使用cgroups实现相同的操作,并限制stress -m的内存使用。

  5. (进阶题)执行curl ipinfo.io获取关于你 IP 的信息。打开 Wireshark 抓取curl发起的请求和收到的回复报文(提示:可以使用http过滤条件,只显示 HTTP 报文)。

延伸阅读

  • 本文对应仓库中的原始文档为 _2020/debugging-profiling.md,配套示例程序见 static/files/logger.py 与 static/files/sorts.py;
  • 与本讲相关的课程还包括Shell 工具和脚本(shellcheck、fd/find对比)与数据整理(日志的过滤与sed流编辑);
  • 仓库还收录了 2026 年新版讲座的英文原文 _2026/debugging-profiling.md,其中补充了可逆调试(rr)、AddressSanitizer 等内存消毒器、eBPF/bpftrace、AI 辅助调试等更新内容,可作为进阶参考。

调试与性能分析的完整工作流可以概括为:先用日志与系统日志快速定位方向(journalctl/log show/logger),再上调试器精确跟踪(pdb/gdb),黑盒场景用strace/tcpdump观察系统调用与网络包,编译期问题交给静态分析与 linter;性能侧则先time计时、htop/free/iotop看资源,再用cProfile/line_profiler/memory-profiler/perf定位热点,最后以火焰图、调用图可视化并以hyperfine做基准对比。掌握这套工具链,你就能系统性地把"写出来的代码"打磨成"想要的代码"。

  • 文档
  • 教程
  • 教育

【免费下载链接】missing-semester-cn.github.io

the CS missing semester Chinese version

项目地址:https://gitcode.com/gh_mirrors/mi/missing-semester-cn.github.io
点击查看免费下载

相关推荐

上一篇:WSL 容器 SDK 的 ImageProgress 数据类:镜像 pull/import/load/push 进度上报机制详解
下一篇:ngx-admin 日期格式化:自定义管道与国际化处理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询