YieldPoint是Managed Language虚拟机实现中一个重要的技术点,恰好最近梳理内存管理技术,发现ISMM 2015年一篇论文《Stop and go: understanding yieldpoint behavior》,是一篇非常不错的入门学习材料。本篇论文也是我们项目组专家王坤山发表的一篇文章,一些不懂的问题刚好可以找他咨询。尽管yieldpoint是个独立的技术,但是内存管理GC是依赖这个技术的。
1,什么是Yield Point?
A yieldpoint is a frequently executed check by managed application code in high performance managed run-time systems, used to determine when a thread must yield。虚拟机(例如JVM)在执行managed code(如java bytecode)时候,thread需要在一些点停下来去做一些任务,例如GC,用户态线程主动调度,解释代码被替换为编译代码等。如何高效实现线程的停顿机制,就是YieldPoint技术。
Yield Point在不同的虚拟机也被称为CheckPoint,SafePoint等。
2,如何实现Yield Point?
本论文中提到了三种实现Yield Point的方法,应该也是目前可行的所有方法。
2.1 实现YieldPoint有三种方法
方法1:a conditional guarded by a state variable
编译器或者解释器执行的时候,比较一个全局变量的值。如果值满足一定条件,则调用处理yieldpoint的代码(The compiler injects a constant comparison against the value of the variable and a conditional jump to the slow path on true)。
此方法的优点是:flexibility,全局变量值可以是多种,不同的场景可以设置不同的值,然后调用不同处理yieldpoint的函数。缺点应该是因为yieldpoint本身很多,导致codesize变大。
方法2:an unconditional load or store from/to a guard page
通过load/write指令读写一个guard page内存,正常的时候此guardpage属性设置为可读或可写,当需要执行yieldpoint的时候,通过设置此page属性不可读写,然后会触发OS系统的SEGV signal,进入注册的信号处理函数,信号处理函数通过判断当前执行的指令序列或者发生问题的地址,去判断是否触发了yieldpoint,然后进行特定的处理函数。
方法3:code patching
NOP patching. 编译器在编译的时候插入nop指令在特定的位置,当需要yieldpoint的时候,动态替换这些NOP指令为yieldpoint的处理函数。此种方法的优势是执行的时候性能开销非常低,但是替换nop指令耗时。同时也不适用类似方舟编译器这种HOST AOT编译器场景。
2.2 插入YieldPoint的时机:
yieldpoints typically occur on loop back edges and on method prologs or epilogs of the application。虚拟机一般有三种执行代码的方式:解释模式,JIT模式及其AOT预编译
JIT/AOT模式:通过编译器产生机器指令,在合适的位置插入YeildPoint指令
解释器模式:解释执行完每个字节码之后去check是否需要Yield。可能不同的虚拟机check的位置不同。
2.3 Scope分类
Global YieldPoint:turned on and off for all application thread,适合stop the world场景。可以用a global conditional variable, a single global protected page
Thread-Scope YieldPoint:turned on and off for a single thread or a group of threads,适合一个暂停一个线程或者一组线程。可以用a thread-local condition variable or thread-local protected page。
Group-based Yieldpoints:暂停一组线程,常用的实现方法是基于条件变量或者code patching的方式实现。
3,YieldPoint实现举例
3.1 ART虚拟机
ART虚拟机YieldPoint被称为CheckPoint/SuspendCheck。ART实现的基于thread-scope的YieldPoint。在每个线程的tls变量中,增加一个指针变量/状态:YieldPoint定义:
//定义在thread.h // In certain modes, setting this to 0 will trigger a SEGV and thus a suspend check. // It is normally set to the address of itself. uintptr_t* suspend_trigger;
// Trigger a suspend check by making the suspend_trigger_ TLS value an invalid pointer. // The next time a suspend check is done, it will load from the value at this address // and trigger a SIGSEGV. void TriggerSuspend() {
tlsPtr_.suspend_trigger = nullptr;
}
//设置成自己的地址 void RemoveSuspendTrigger() {
tlsPtr_.suspend_trigger = reinterpret_cast(&tlsPtr_.suspend_trigger);
}
template
static constexpr ThreadOffset ThreadSuspendTriggerOffset() {
return ThreadOffsetFromTlsPtr(
OFFSETOF_MEMBER(tls_ptr_sized_values, suspend_trigger));
}
...
union StateAndFlags state_and_flags;YieldPoint的指令插入:
1. JIT/AOT编译器插入
ART使用的IR SuspendCheck,这块代码没有仔细研究,代码主要集中在:
// optimizing/code_generator_arm64.cc:
void InstructionCodeGeneratorARM64::GenerateSuspendCheck(HSuspendCheck* instruction,
...
// 产生testsuspend指令
void EmitNativeCode(CodeGenerator* codegen) override {
LocationSummary* locations = instruction_->GetLocations();
CodeGeneratorARM64* arm64_codegen = down_cast(codegen);
__ Bind(GetEntryLabel());
SaveLiveRegisters(codegen, locations); // Only saves live 128-bit regs for SIMD.
arm64_codegen->InvokeRuntime(kQuickTestSuspend, instruction_, instruction_->GetDexPc(), this);
CheckEntrypointTypes();
RestoreLiveRegisters(codegen, locations); // Only restores live 128-bit regs for SIMD.
if (successor_ == nullptr) {
__ B(GetReturnLabel());
} else {
__ B(arm64_codegen->GetLabelOf(successor_));
}
}
// quick_entrypoints_list.h
//art/runtime/entrypoints/quick/quick_default_init_entrypoints.h
// Thread
qpoints->pTestSuspend = art_quick_test_suspend;
//register_allocator_linear_scan.cc
2. 解释器:调用MterpSuspendCheck函数
inline void Thread::AllowThreadSuspension() {
DCHECK_EQ(Thread::Current(), this);
if (UNLIKELY(TestAllFlags())) {
CheckSuspend();
}
}
3. Runtime:主动调用SuspendCheck函数RequestCheckpoint/RequestEmptyCheckpoint/ModifySuspendCountInternal中按照不同场景也会调用触发suspend。
YieldPoint触发处理:ART虚拟机处理AOT/JIT YieldPoint通过触发SIGSEGV处理(ARR已经废弃此方法,implicit_suspend_checks_选项已经没有用了)
// ARM64为例
// art/runtime/arch/arm64/fault_handler_arm64.cc
bool SuspensionHandler::Action(int sig ATTRIBUTE_UNUSED, siginfo_t* info ATTRIBUTE_UNUSED,
void* context) {
....
//判断触发的SIGSEGV是不是yieldpoint指令
1. 首先判断是不是ldr x0, [x0]
2. 然后向上搜索ldr x0, [x18, ,#xxx],x18是tls线程寄存器,x18+offset是suspend_trigger偏移
//如果匹配
pc设置为art_quick_implicit_suspend执行挂载函数
//恢复
}
2. ART另外一种方式通过a conditional guarded by a state variable
上面也阐述了,其实调用的是art_quick_test_suspend
3. 解释器模式
直接调用CheckSuspend处理对应的CheckPoint函数。
3.2 方舟编译器
目前方舟编译器是全静态编译器,yieldpoint的实现方式是方法2,即通过编译器插入指令访问guard page触发信号。编译器插入可以参考开源代码:https://gitee.com/openarkcompiler-incubator/mapleall/tree/master/maple_be/src/cg/aarch64/aarch64_cg_func.cpp
void AArch64CGFunc::GenerateYieldpoint(BB * bb) {
// ldr wzr, [RYP] # RYP hold address of the polling page. auto &wzr = AArch64RegOperand::Get32bitZeroRegister();
auto pollingPage = CreateMemOpnd(RYP, 0, 32);
auto yieldPoint = cg->BuildInstruction(MOP_wldr, &wzr, pollingPage);
if (cg->GenerateVerboseAsm()) {
yieldPoint->AddComment("yieldpoint");
}
bb->AppendInsn(yieldPoint);
}
编译器yieldpoint规则:
AArch64CGFunc::HandleRCCall:在函数头所有引用变量初始化完以后插入
AArch64YieldPointInsertion::InsertYieldPoint:Epilogue/循环回边插入
运行时还未开源:
通过注册SIGSEGV 信号处理函数,判断触发信号的地址是否polling page?如果是,则发生了yieldpoint,处理寄存器保存等各种操作。
auto &wzr = AArch64RegOperand::Get32bitZeroRegister();
ldr wzr, [RYP] # RYP hold address of the polling page.
运行时将页面设置为不可读。
参考:(PDF) Stop and go: understanding yieldpoint behaviorwww.researchgate.netART是如何保证checkpoint 点一定会被跑到的?www.zhihu.com