第57篇-profile實例
- 2022 年 1 月 20 日
- 筆記
之前已經介紹過回邊計數和ProfileData與Layout,下面舉個具體的例子看下MethodData是怎麼利用ProfileData等記錄詳細的運行時信息的。實例如下:
package com.test; import java.util.LinkedList; public class CompilationDemo { public static void main(String args[]){ fact(60010*2); } public static int fact(int n) { int p = 1; while (n > 0) { p++; } return p; } }
通過如下命令配置讓HotSpot VM運行如上使用Javac編譯的位元組碼,如下:
-cp .:/media/mazhi/sourcecode/workspace/projectjava/projectjava01/bin -XX:+TraceOnStackReplacement com.test/CompilationDemo
生成的位元組碼如下:
public static int fact(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=1 0: iconst_1 1: istore_1 2: iload_0 3: ifle 12 // 當棧頂int類型數值小於等於0時跳轉 6: iinc 1, 1 9: goto 2 12: iload_1 13: ireturn
對如上的位元組碼來說,位元組碼索引為3和9的2個位元組碼 ifle和goto需要有對應的ProfileData,具體就是BranchData和JumpData。其數據布局如下:
BranchData、JumpData等都是按照DataLayout的格式布局數據的,DataLayout在前一篇文章中詳細介紹過,如下:
class DataLayout VALUE_OBJ_CLASS_SPEC { private: union { // intptr_t類型佔用8個位元組 intptr_t _bits; struct { u1 _tag; // flags的格式為[ recompile:1 | reason:3 | flags:4] u1 _flags; u2 _bci; } _struct; } _header; // 可以有許多個cells,首個cell的地址通過如下的_cells屬性保存, // 具體的cells數組的大小還要根據具體的ProfileData來決定 intptr_t _cells[1]; // ... }
對於本篇的實例來說,各個具體的值如下:
(1)BranchData
DataLayout::_bits=0 DataLayout::_struct._tag=DataLayout::branch_data_tag _header._struct._bci=3
_cells數組的大小為3,每個數組元素的大小為8位元組。_cells的初始化主要是在各個ProfileData的子類中調用post_initialize()函數完成的,對於BranchData來說,調用BranchData類的post_initialize()函數初始化。_cells在初始化時會在下標為displacement_off_set=1處存儲56。因為ifle指令跳轉的目的地指令沒有對應的ProfileData數據,所以直接跳轉出了data_size區域。
BranchData類的定義如下:
class BranchData : public JumpData { protected: enum { not_taken_off_set = jump_cell_count, // jump_cell_count的值為2 branch_cell_count // branch_cell_count的值為3 }; // ... }
注意BranchData類繼承自JumpData類,所以BranchData類的_cells需要3個,分別為JumpData::taken_off_set、JumpData::displacement_off_set和BranchData::not_taken_off_set。
調用如下函數初始化BranchData,函數的實現如下:
void BranchData::post_initialize(BytecodeStream* stream, MethodData* mdo) { assert(stream->bci() == bci(), "wrong pos"); int target = stream->dest(); // 調用dp()函數獲取ProfileData::_data屬性的值,然後結合MethodData::_data計算 // 出BranchData相對於MethodData::_data的索引值 int my_di = mdo->dp_to_di(dp()); // 通過位元組碼指令的下標索引獲取target data index int target_di = mdo->bci_to_di(target); int offset = target_di - my_di; // 將偏移存儲到JumpData的displacement_off set_displacement(offset); }
可以看到會初始化BranchData中的JumpData::displacement_off_set屬性的值。其它_cells的值為0。
(2)JumpData
DataLayout::_bits=0 DataLayout::_struct._tag=DataLayout::jump_data_tag _header._struct._bci=9
_cells數組的大小為2,初始化時在下標為displacement_off_set=1處存儲-32。因為goto指令會跳轉到下標索引為2的位元組碼處,這個指令雖然沒有ProfileData,但是ifle有對應的BranchData數據,所以JumpData的_data_index加上-32後值為0,在運行過程中遇到ifle時直接能通過_data_index找到對應的BranchData。
JumpData類的定義如下:
class JumpData : public ProfileData { protected: enum { taken_off_set, // 0 displacement_off_set, // 1 jump_cell_count // 2 }; // ... }
JumpData佔用的內存大小除了DataLayout::_header之外,還要分配2個cell,每個cell的大小為8位元組,分別用來存儲taken_off和displacement_off。taken_off表示跳轉的次數,而displacement_off用來調整method data pointer到對應的ProfileData位置,這個ProfileData位置和跳轉的位元組碼指令對應,這樣如果跳轉的目標位元組碼指令如果也有一些需要記錄的信息,則直接通過method data pointer就能找到對應的ProfileData進行記錄。
JumpData::post_initialize()函數的實現如下:
void JumpData::post_initialize(BytecodeStream* stream, MethodData* mdo) { int target; Bytecodes::Code c = stream->code(); // 獲取跳轉指令的目標跳轉位元組碼指令的索引 if (c == Bytecodes::_goto_w || c == Bytecodes::_jsr_w) { target = stream->dest_w(); } else { target = stream->dest(); } // dp()函數獲取JumpData::_data的首地址,然後結合MethodData::_data計算 // 出JumpData在MethodData::_data的索引值 int my_di = mdo->dp_to_di(dp()); // // 通過位元組碼指令的下標索引獲取target data index int target_di = mdo->bci_to_di(target); int offset = target_di - my_di; // 將偏移存儲到JumpData的displacement_off set_displacement(offset); }
可以看到JumpData中的displacement_off存儲的是_data的偏移量,method data pointer其實是指向_data中的某一項ProfileData,偏移 displacement_off 後仍然指向另外一個ProfileData。
(3)ArgInfoData
DataLayout::_bits=0 DataLayout::_struct._tag=DataLayout::arg_info_data_tag _header._struct._bci=0
_cells數組的大小為2。在下標為0處的數組中存儲的是數組的長度,為1。可以理解為下標索引最大為1。
下面看一下,控制轉移指令是如何通過Method::_method_data中的_data屬性記錄運行時信息的。
ifle指令對應的彙編代碼如下:
// 對第1個第2個操作數進行邏輯與,如果為0則ZF設置為0 0x00007fffe101ba07: test %eax,%eax // 如果大於0則跳轉到---- not_taken ---- 0x00007fffe101ba09: jg 0x00007fffe101bd69 // 找到Method*並存儲到%rcx中 0x00007fffe101ba0f: mov -0x18(%rbp),%rcx // 找到method data pointer,如果為NULL就直接跳轉 0x00007fffe101ba13: mov -0x20(%rbp),%rax 0x00007fffe101ba17: test %rax,%rax 0x00007fffe101ba1a: je 0x00007fffe101ba38 // 根據Method::_method_data獲取到JumpData::taken_off_set偏移處屬性的值並存儲到%rbx中 0x00007fffe101ba20: mov 0x8(%rax),%rbx // 增加DataLayout::counter_increment,值為1 0x00007fffe101ba24: add $0x1,%rbx 0x00007fffe101ba28: sbb $0x0,%rbx // 存儲回JumpData::taken_off_set偏移處 0x00007fffe101ba2c: mov %rbx,0x8(%rax) // %rax中存儲的是method data pointer // 根據method data pointer獲取JumpData::displacement_off_set偏移處的值 0x00007fffe101ba30: add 0x10(%rax),%rax // 將%rax中存儲的值更新到棧中interpreter_frame_mdx_offset偏向處 0x00007fffe101ba34: mov %rax,-0x20(%rbp) // .... // **** not_takne **** // 如果method data pointer為NULL,就直接跳轉到---- profile_continue ---- 0x00007fffe101bd69: mov -0x20(%rbp),%rax 0x00007fffe101bd6d: test %rax,%rax 0x00007fffe101bd70: je 0x00007fffe101bd88 // 增加BranchData::not_taken_off_set=2處的值,加1 0x00007fffe101bd76: addq $0x1,0x18(%rax) // 根據method data pointer增加$0x20,也就是BranchData的大小 0x00007fffe101bd7b: sbbq $0x0,0x18(%rax) 0x00007fffe101bd80: add $0x20,%rax 0x00007fffe101bd84: mov %rax,-0x20(%rbp) // **** profile_continue ****
要注意,如上的method data pointer指向的是_data_index為0的位置的地址。對於BranchData來說,_cells數組的大小為3,分別存儲着JumpData::taken_off_set、JumpData::displacement_off_set和BranchData::not_taken_off_set,所以會詳細記錄下相關指令的運行時具體數據,非常有利於後續編譯器進行高級優化。
在介紹goto位元組碼指令時,調用的TemplateTable::branch()函數中會調用InterpreterMacroAssembler::profile_taken_branch()函數,生成的彙編代碼如下:
// 如果開啟了選項ProfileInterpreter,則執行分支跳轉相關的性能統計 // %rax中保存着mdp(method data pointer) 0x00007fffe101dd14: mov -0x20(%rbp),%rax // 如果Method::_method_data的值為NULL,則跳轉到---- profile_continue ---- 0x00007fffe101dd18: test %rax,%rax 0x00007fffe101dd1b: je 0x00007fffe101dd39 // 代碼執行到這裡時,表示Method::_method_data的值不為NULL // 根據Method::_method_data獲取到JumpData::taken_off_set偏移處屬性的值並存儲到%rbx中 0x00007fffe101dd21: mov 0x8(%rax),%rbx // 增加DataLayout::counter_increment,值為1 0x00007fffe101dd25: add $0x1,%rbx // sbb是帶借位減法指令 0x00007fffe101dd29: sbb $0x0,%rbx // 存儲回JumpData::taken_off_set偏移處 0x00007fffe101dd2d: mov %rbx,0x8(%rax) // %rax中存儲的是method data pointer // 根據method data pointer獲取JumpData::displacement_off_set偏移處的值 0x00007fffe101dd31: add 0x10(%rax),%rax // 將%rax中存儲的值更新到棧中interpreter_frame_mdx_offset偏向處 0x00007fffe101dd35: mov %rax,-0x20(%rbp)
當Method::_method_data不為NULL時,會向MethodData::_data中記錄控制轉移的次數(注意這裡是控制轉移的次數,並不是回邊的次數)。通過JumpData來記錄,這個JumpData已經在MethodData::_data上的對應位置上並且已經進行了初始化,,在DataLayout::initialize()函數中初始化一些常用的屬性,然後調用post_initialize()函數完成一些特定屬性的初始化,下面看一下JumpData。
程序在使用過程中,經常需要在bci、bcp、method data pointer(棧中interpreter_frame_mdx_offset處存儲的就是這個值)、data index。如bci轉換為data index的函數如下:
int bci_to_di(int bci) { address x = bci_to_dp(bci); return dp_to_di(x); }
首先要通過bci找到method data pointer,調用的函數如下:
address MethodData::bci_to_dp(int bci) { ResourceMark rm; ProfileData* data = data_before(bci); ProfileData* prev = NULL; for ( ; is_valid(data); data = next_data(data)) { if (data->bci() >= bci) { // 如果進入這個循環,則一定會返回 if (data->bci() == bci){ int x = dp_to_di(data->dp()); set_hint_di(x); } else if (prev != NULL){ int x = dp_to_di(prev->dp()); set_hint_di(x); } return data->dp(); } prev = data; } return (address)limit_data_position(); } ProfileData* data_before(int bci) { // avoid SEGV on this edge case if (data_size() == 0){ return NULL; } int hint = hint_di(); if (data_layout_at(hint)->bci() <= bci){ return data_at(hint); } return first_data(); }
將method data pointer轉換為data index的函數如下:
int dp_to_di(address dp) { return dp - ((address)_data); }
有時候,當Method::_method_data屬性的值不為NULL時需要調用InterpreterRuntime::bcp_to_di()函數將bcp轉換為data index,此函數的實現如下:
IRT_LEAF(jint, InterpreterRuntime::bcp_to_di( Method* method, address cur_bcp) ) int bci = method->bci_from(cur_bcp); MethodData* mdo = method->method_data(); if (mdo == NULL) return 0; return mdo->bci_to_di(bci); IRT_END
首先調用bci_from()函數獲取位元組碼索引bci,函數的實現如下:
int Method::bci_from(address bcp) const { return bcp - code_base(); }
然後調用bci_to_di()函數即可。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流