Hacker News 中文摘要

RSS订阅

40行代码修复消除400倍性能差距 -- A 40-line fix eliminated a 400x performance gap

文章摘要

一位开发者浏览OpenJDK提交日志时,发现一个优化线程CPU时间获取的提交:用clock_gettime替代读取/proc文件系统的方法。原方法需打开文件、解析内容,新方法仅40行代码就消除了400倍的性能差距,同时减少了代码量。这展示了简单优化能带来显著性能提升。

文章总结

40行代码如何消除400倍的性能差距

我有个习惯,每隔几周就会浏览OpenJDK的提交日志。虽然很多提交内容过于复杂,难以在有限时间内完全理解,但偶尔会有一些引人注目的内容。最近,这个提交引起了我的注意:

858d2e434dd:8372584: [Linux]:用clock_gettime替代读取/proc获取线程CPU时间

提交的代码变动很有趣:增加了96行,删除了54行。其中新增的55行是JMH基准测试代码,实际的生产代码反而减少了。

被删除的代码

以下是os_linux.cpp中被移除的user_thread_cpu_time函数实现:

cpp static jlong user_thread_cpu_time(Thread *thread) { pid_t tid = thread->osthread()->thread_id(); char stat[2048]; FILE *fp = fopen(proc_name, "r"); // ...(省略部分代码) sscanf(s, "%c %d %d ... %lu %lu", ..., &user_time, &sys_time); return (jlong)user_time * (1000000000 / clock_tics_per_second); }

这段代码是ThreadMXBean.getCurrentThreadUserTime()的底层实现,其流程如下: 1. 拼接路径/proc/self/task/<tid>/stat; 2. 打开文件并读取内容到缓冲区; 3. 解析复杂的文本格式(命令名可能包含括号); 4. 用sscanf提取第13和14个字段(用户时间和系统时间); 5. 将时钟周期转换为纳秒。

相比之下,getCurrentThreadCpuTime()的实现一直很简单: cpp jlong os::current_thread_cpu_time() { return os::Linux::thread_cpu_time(CLOCK_THREAD_CPUTIME_ID); } 仅调用一次clock_gettime(),无需文件I/O或复杂解析。

性能差距

2018年的原始问题报告指出:

getCurrentThreadUserTimegetCurrentThreadCpuTime30到400倍

在高并发场景下,差距更明显。原因在于: - /proc路径:涉及多次系统调用、VFS调度、内核字符串格式化、用户空间解析等。 - clock_gettime路径:仅需一次系统调用,直接读取内核的sched_entity数据结构。

为何最初不用clock_gettime

POSIX标准规定CLOCK_THREAD_CPUTIME_ID返回总CPU时间(用户+系统),而getCurrentThreadUserTime需要仅用户时间。因此早期只能通过/proc实现。

解决方案:时钟ID位操作

Linux内核(2.6.12+)在clockid_t中编码了时钟类型信息: - 低2位表示类型:01为用户时间(VIRT),10为POSIX兼容的总时间(SCHED)。 - 通过修改clockid的低位,可以绕过POSIX限制,直接获取用户时间。

新实现仅需40行代码,无需文件操作或解析: cpp static jlong user_thread_cpu_time(Thread *thread) { clockid_t clockid; bool success = get_thread_clockid(thread, &clockid, false); return success ? os::Linux::thread_cpu_time(clockid) : -1; }

性能对比

基准测试结果: - 修复前:平均每次调用耗时11微秒。 - 修复后:平均降至279纳秒,提升40倍(接近原始报告的30-400倍范围)。

进一步优化

分析发现,内核通过clockid中的线程ID查找pid结构时,若ID为0则直接跳过查找(快速路径)。手动构造clockid(PID=0)可再减少13%耗时(平均从81.7纳秒降至70.8纳秒)。

总结

  • 内核源码是宝藏:POSIX定义可移植性,但内核实现可能提供更高性能的方案。
  • 定期验证旧假设/proc解析在早期合理,但随着内核功能演进可能过时。
  • 影响:该优化随JDK 26(2026年3月发布)生效,为相关调用带来免费性能提升。

(注:原文中的图片链接和部分技术细节已简化,核心逻辑和关键数据保留。)

评论总结

这篇评论主要围绕JVM线程CPU时间统计的性能优化展开讨论,包含技术分析和用户反馈:

  1. 性能问题发现
    作者指出获取线程CPU时间的操作成本过高(评论1:"What is the CPU time of this thread? is/was a much more expensive question than it should be")

  2. 技术优化方案

  • 使用vDSO避免上下文切换(评论3:"clock_gettime() goes through vDSO, avoiding a context switch")
  • 建议采用软件性能事件实现10倍优化(评论4:"about 8ns (almost an additional 10x improvement) by using software perf events")
  1. 用户正面反馈
  • 对文章和团队的赞赏(评论5:"The QuestDB team are among the best doing it")
  • 肯定优化效果(评论7:"how a small change can outweigh years of incremental tuning")
  1. 火焰图应用价值
  • 赞赏交互式SVG设计(评论6:"did not expect 'Open Image in New Tab' to do what it said")
  • 揭示代码问题的有效性(评论8:"found all kinds of crazy stuff in codebases this way")

注:所有评论均未显示评分,但包含具体技术讨论(评论3/4/8)和情感表达(评论2/5/6)两种类型。