Python 提供了多种看似相近的计时函数,但它们回答的问题并不相同:time.time() 适合记录现实世界中的时间点,time.monotonic() 适合判断超时,而 time.perf_counter() 更适合测量代码运行时长。选错时钟,轻则让性能数据失真,重则让超时逻辑受到系统时间调整的影响。
先明确:你要测的是时间点,还是时间间隔
选择函数之前,可以先问自己一个问题:结果需要对应日历时间,还是只关心经过了多久?
| 函数 | 典型用途 | 是否适合测量耗时 | 关键特性 |
|---|---|---|---|
time.time() |
时间戳、日志时间、跨系统交换时间点 | 不优先推荐 | 系统时钟可能被校准或手动修改 |
time.monotonic() |
超时、重试窗口、调度间隔 | 是 | 单调递增,不受系统时间回拨影响 |
time.perf_counter() |
性能测量、短代码段计时 | 是,通常优先 | 使用高分辨率性能计数器 |
time.process_time() |
测量当前进程消耗的 CPU 时间 | 视目标而定 | 不计算睡眠等等待时间 |
例如,一个函数既执行计算又等待网络响应时,perf_counter() 测到的是用户实际等待的总时长;process_time() 更接近进程真正占用 CPU 的时间。两者不是谁更准确,而是在回答不同的问题。
import time
wall_start = time.perf_counter()
cpu_start = time.process_time()
time.sleep(0.2)
sum(i * i for i in range(500_000))
wall_elapsed = time.perf_counter() - wall_start
cpu_elapsed = time.process_time() - cpu_start
print(f"实际经过时间: {wall_elapsed:.6f} 秒")
print(f"进程 CPU 时间: {cpu_elapsed:.6f} 秒")
运行后,实际经过时间会包含约 0.2 秒的睡眠,而 CPU 时间通常不会。这正是 I/O 延迟分析与 CPU 热点分析之间的区别。
超时逻辑不要依赖墙上时钟
time.time() 返回的是 Unix 时间戳,适合写入日志或生成带时间信息的记录。但系统时钟可能因人工修改或时间同步而向前、向后跳变,因此不应把它作为超时判断的首选。
超时逻辑可以这样实践:
import time
def wait_until_ready(check, timeout=2.0, interval=0.05):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if check():
return True
time.sleep(interval)
return False
started_at = time.monotonic()
def demo_check():
return time.monotonic() - started_at >= 0.3
if wait_until_ready(demo_check, timeout=1.0):
print("资源已就绪")
else:
print("等待超时")
这里真正重要的不是精度,而是单调性。即使操作系统校准了当前日期和时间,截止时间与当前计时值之间的比较仍然可靠。
用上下文管理器封装代码块计时
当项目中有多个代码块需要计时,反复手写开始时间和结束时间很容易遗漏异常路径,也容易混用不同的时钟。一个小型上下文管理器可以统一这些行为。
下面的示例使用 perf_counter_ns() 保存整数纳秒值,避免在内部累计时过早引入浮点舍入。复制为 timer_demo.py 后即可直接运行:
import time
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class Timer:
name: str = "代码块"
clock: Callable[[], int] = time.perf_counter_ns
elapsed_ns: int = field(init=False, default=0)
_started_ns: int = field(init=False, default=0)
def __enter__(self):
self._started_ns = self.clock()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed_ns = self.clock() - self._started_ns
print(f"{self.name}: {self.elapsed_ns / 1_000_000:.3f} ms")
return False
with Timer("生成平方和") as timer:
result = sum(i * i for i in range(1_000_000))
print("结果:", result)
print("记录的纳秒数:", timer.elapsed_ns)
__exit__() 返回 False,意味着被测代码如果抛出异常,异常仍会继续传播;计时信息则依然能够输出。对于生产环境,可以把 print() 换成结构化日志或指标上报。
如果要让这个工具支持嵌套计时、累计多次结果或异步上下文,还需要进一步设计状态管理,不能简单复用同一个正在运行的 Timer 实例。
微基准测试交给 timeit
单次 perf_counter() 测量很容易受到解释器预热、垃圾回收、操作系统调度和后台进程的干扰。比较两个短小实现时,使用标准库 timeit 重复执行通常更合适。
可以直接在终端运行:
python -m timeit -s "values = list(range(1000))" "sum(x * x for x in values)"
python -m timeit -s "values = list(range(1000))" "sum([x * x for x in values])"
也可以在 Python 中调用:
import timeit
seconds = timeit.timeit(
"sum(x * x for x in values)",
setup="values = list(range(1000))",
number=10_000,
)
print(f"总耗时: {seconds:.6f} 秒")
print(f"平均每次: {seconds / 10_000 * 1_000_000:.3f} 微秒")
不过,微基准更快并不自动意味着真实应用更快。数据库、网络、磁盘、缓存命中率和并发竞争都可能改变最终结果。应当先用分析工具找到热点,再针对热点设计足够接近生产负载的基准。
一份实用选择清单
可以用下面的规则快速决策:
- 需要日志中的日期或可交换的 Unix 时间戳:使用
time.time()或datetime。 - 需要实现超时、退避和截止时间:使用
time.monotonic()。 - 需要测量一段代码实际经过的时间:使用
time.perf_counter()。 - 只关心进程消耗的 CPU 时间:使用
time.process_time()。 - 需要比较非常短的 Python 表达式:优先使用
timeit,并执行多轮测试。 - 需要记录业务阶段耗时:封装上下文管理器,并把结果接入日志或监控系统。
计时函数的关键并不是记住哪个名字“精度最高”,而是理解每种时钟的语义。先确定要观察的是现实时间、单调间隔、墙钟耗时还是 CPU 消耗,再选择对应工具,测量结果才有解释价值。