当忙于旋转的Java线程绑定到物理内核时,是不是会因为到达代码中的新分支而发生上下文切换?

Posted

技术标签:

【中文标题】当忙于旋转的Java线程绑定到物理内核时,是不是会因为到达代码中的新分支而发生上下文切换?【英文标题】:When busy-spining Java thread is bound to physical core, can context switch happen by the reason that new branch in code is reached?当忙于旋转的Java线程绑定到物理内核时,是否会因为到达代码中的新分支而发生上下文切换? 【发布时间】:2021-01-31 04:38:37 【问题描述】:

我对低延迟代码感兴趣,这就是我尝试配置线程亲和性的原因。特别是,它应该有助于避免上下文切换。

我已经使用https://github.com/OpenHFT/Java-Thread-Affinity 配置了线程关联。我运行非常简单的测试代码,它只是在一个循环中检查时间条件。

    long now = start;
    while (true)
    
        if (now < start + TimeUtils.NANOS_IN_SECOND * delay)
        
            now = TimeUtils.now();
        
        else
        
            // Will be printed after 30 sec
            if (TimeUtils.now() > start + TimeUtils.NANOS_IN_SECOND * (delay + 30))
            
                final long finalNow = now;
                System.out.println("Time is over at " +
                        TimeUtils.toInstant(finalNow) + " now: " +
                        TimeUtils.toInstant(TimeUtils.now()));
                System.exit(0);
            
        
    

因此,在指定的延迟执行后转到“else”,大约在同一时间,我看到了上下文切换。这是预期的行为吗?这样做的具体原因是什么?在这种情况下如何避免上下文切换?

测试详情

我从这个 repo 构建 shadowJar:https://github.com/stepan2271/thread-affinity-example。然后我使用以下命令运行它(可以在这里玩弄数字,延迟> 60时对测试没有显着影响):

taskset -c 19 java -DtestLoopBindingCpu=3 -Ddelay=74 -cp demo-all.jar main.TestLoop

我还有以下测试脚本来监控上下文切换(应该使用绑定到核心的 Java 线程的 ID 运行)

#!/bin/bash
while [ true ]
do
date >> ~/demo-ctxt-switches.log
cat /proc/$1/status | grep ctxt >> ~/demo-ctxt-switches.log
sleep 3
done

此脚本的典型输出如下:

Fri Oct 16 18:23:29 MSK 2020
voluntary_ctxt_switches:    90
nonvoluntary_ctxt_switches: 37
Fri Oct 16 18:23:32 MSK 2020
voluntary_ctxt_switches:    90
nonvoluntary_ctxt_switches: 37
Fri Oct 16 18:23:35 MSK 2020
voluntary_ctxt_switches:    90
nonvoluntary_ctxt_switches: 37
Fri Oct 16 18:23:38 MSK 2020
voluntary_ctxt_switches:    90
nonvoluntary_ctxt_switches: 37
Fri Oct 16 18:23:41 MSK 2020
voluntary_ctxt_switches:    91
nonvoluntary_ctxt_switches: 37
Fri Oct 16 18:23:44 MSK 2020
voluntary_ctxt_switches:    91
nonvoluntary_ctxt_switches: 37
Fri Oct 16 18:23:47 MSK 2020
voluntary_ctxt_switches:    91
nonvoluntary_ctxt_switches: 37

因此,在开始时间发生一些变化后,这些数字变得稳定,然后我看到代码到达“else”分支时正好有 1 到 3 个开关(差异小于 1 秒)。

偏差

基本配置几乎每次都会重现这种行为,而有些偏差会导致我无法重现的情况。例子:

https://github.com/stepan2271/thread-affinity-example/tree/without-log4j

https://github.com/stepan2271/thread-affinity-example/tree/without-cached-nano-clock

测试环境

2 * Intel(R) Xeon(R) Gold 6244 CPU @ 3.60GHz

红帽企业 Linux 8.1 (Ootpa)

使用 /etc/systemd/system.conf 和 /etc/systemd/user.conf 中的 CPUAffinity 隔离核心

/etc/sysconfig/irqbalance 已配置。

Openjdk 11.0.6 2020-01-14 LTS 运行时环境 18.9

【问题讨论】:

【参考方案1】:

自愿上下文切换通常意味着线程正在等待某事,例如让锁变得空闲。

async-profiler 可以帮助找到上下文切换发生的位置。这是我使用的命令行:

./profiler.sh -d 80 -e context-switches -i 2 -t -f switches.svg -I 'main*' -X 'exit_to_usermode_loop*' PID

让我们详细介绍一下:

-d 80 运行分析器最多 80 秒。 -e context-switches 要分析的事件。 -i 2 间隔 = 2 个事件。我每隔一秒进行一次上下文切换分析,因为分析信号本身会导致上下文切换,我不想陷入递归。 -t 按线程拆分配置文件。 -f switches.svg输出文件名; svg 扩展自动选择火焰图格式。 -I 'main*' 在输出中仅包含主线程。 -X 'exit_to_usermode_loop*' 排除与非自愿上下文切换相关的事件。 PID 要分析的 Java 进程 ID。

每次运行的结果可能会有所不同。通常我会在每个图表上看到 0 到 3 个上下文切换。

这里是发生上下文切换的最常见的地方。它们确实与等待互斥锁有关。

    ThreadSafepointState::handle_polling_page_exception()TestLoop.main 调用。这意味着,一个线程已在另一个线程请求的安全点处停止。要调查安全点的原因,请添加 -Xlog:safepoint* JVM 选项。
[75.889s][info][safepoint        ] Application time: 74.0071000 seconds
[75.889s][info][safepoint        ] Entering safepoint region: Cleanup
[75.889s][info][safepoint,cleanup] deflating idle monitors, 0.0000003 secs
[75.889s][info][safepoint,cleanup] updating inline caches, 0.0000058 secs
[75.890s][info][safepoint,cleanup] compilation policy safepoint handler, 0.0000004 secs
[75.890s][info][safepoint,cleanup] purging class loader data graph, 0.0000001 secs
[75.890s][info][safepoint,cleanup] resizing system dictionaries, 0.0000009 secs
[75.890s][info][safepoint,cleanup] safepoint cleanup tasks, 0.0001440 secs
[75.890s][info][safepoint        ] Leaving safepoint region

没错,清理安全点在 74 秒后不久发生(正是指定的延迟)。 Cleanup safepoint 的目的是运行周期性任务;在这种情况下 - 更新内联缓存。如果有清理工作要做,安全点可能每GuaranteedSafepointInterval 毫秒(默认为 1000)发生一次。你可以通过设置-XX:GuaranteedSafepointInterval=0来禁用周期性安全点,但这可能有performance implications。

    SharedRuntime::handle_wrong_method() 来自 TimeUtils.now。当编译代码中的调用站点变为非进入时,就会发生这种情况。由于这与 JIT 编译有关,请添加 -XX:+PrintCompilation 选项。
  75032 1430 %     4       main.TestLoop::main @ 149 (245 bytes)   made not entrant
  75033 1433 %     3       main.TestLoop::main @ 149 (245 bytes)
  75033 1434       4       util.RealtimeNanoClock::nanoTime (8 bytes)
  75034 1431       3       util.RealtimeNanoClock::nanoTime (8 bytes)   made not entrant
  75039 1435 %     4       main.TestLoop::main @ 149 (245 bytes)
  75043 1433 %     3       main.TestLoop::main @ 149 (245 bytes)   made not entrant

是的,TestLoop.mainRealtimeNanoClock.nanoTime 在 JVM 启动 75 秒后重新编译。要查找原因,请添加-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation

这将生成一个大型编译日志,我们将在其中查找在第 75 秒发生的事件。

<uncommon_trap thread='173414' reason='unstable_if' action='reinterpret' debug_id='0' compile_id='1232' compile_kind='osr' compiler='c2' level='4' stamp='75.676'>
<jvms bci='161' method='main.TestLoop main ([Ljava/lang/String;)V' bytes='245' count='1' backedge_count='533402' iicount='1'/>

这是一个uncommon trap,因为unstable_if在字节码索引161处。换句话说,当main被JIT编译时,HotSpot没有为else分支生成代码,因为它之前从未执行过(这种推测性的死代码消除)。但是,为了保持编译代码的正确性,如果推测条件失败,HotSpot 会设置一个陷阱来取消优化并退回到解释器。当if 条件变为false 时,这正是您的情况。

    Runtime1::counter_overflow()。这又与重新编译有关。运行C1编译代码一段时间后,HotSpot发现代码很热,决定用C2重新编译。

    在这种情况下,我在编译器队列上发现了一个竞争锁。

结论

HotSpot JIT 编译器严重依赖推测优化。当推测条件失败时,这会导致去优化。去优化确实对低延迟应用程序非常不利:除了切换到解释器中的缓慢执行之外,这可能会间接导致由于在 JVM 运行时中获取锁或将 JVM 带到安全点而导致不希望的暂停。

去优化的常见原因是unstable_ifclass_check。如果您想避免对延迟关键路径进行去优化,请确保为虚拟方法“预热”所有代码路径和所有可能的接收器。

【讨论】:

感谢您的详细解答!这说明线程亲和性和上下文切换问题与JVM预热问题是分不开的。建议的工具(async-profiler 和 JVM 标志)的开销如何? (我的意思不仅是对上下文切换的影响,而且是对总体性能的影响) 哇!这个答案真的是shipilev! (对不起,我很傻,“shipilev”是我刚刚发明的一个形容词,用于以非常详细和清晰的方式解释与性能和并发性有关的事情,就像Aleksey Shipilëv 一样.) @stepan2271 性能是相对的——我没有一个通用的答案。日志标志大多是无害的,除非日志被写入慢速磁盘。 Async-profiler 通常开销较低,适合生产使用,但这取决于事件的数量和堆栈深度。 -i(分析间隔)是在详细配置文件和低性能开销之间进行选择的主要开关。

以上是关于当忙于旋转的Java线程绑定到物理内核时,是不是会因为到达代码中的新分支而发生上下文切换?的主要内容,如果未能解决你的问题,请参考以下文章

C#中AutoResetEvent的waitOne()方法会影响到所有用户么?

linux内存管理--linux内核高端内存

使用 openMP 进行多核处理与多线程

跨内核线程迁移后是不是可以强制重新加载 thread_local 变量?

执行线程的核心数

JAVA线程池详解