程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 利用PAPI接口監測Java程序的硬件執行特征

利用PAPI接口監測Java程序的硬件執行特征

編輯:關於JAVA

簡介:PAPI 是一組用於訪問處理器硬件性能計數器的本地接口,利用這一接口對 Java 程序的硬件執 行特征進行監測將有助於在計算機系統的硬件層上發現程序性能問題的根源所在。本文介紹了 PAPI 的重 要概念及其常用接口,分析了將其應用於 Java 程序性能測評的要點和難點,提出一種基於 JVMTI 接口 的方法實現了利用 PAPI 接口監測 Java 程序的運行時硬件執行特征。

PAPI 接口概覽

在計算機系統的硬件層對 Java 程序的性能進行測評與分析,有助於發現程序 性能問題的根源所在。當前的主流處理器大多設定了一類用於記錄程序運行過程中的處理器行為細節的事 件(Event),同時還專門設計了硬件性能計數器(Hardware Performance Counter)對這類事件進行計 數。硬件性能計數器監測到的處理器事件的發生次數能夠直觀地體現程序運行時的硬件執行特征,例如處 理器執行完成的指令數、L1 Cache 的失效次數等等,是測評和分析程序性能的可靠依據。

PAPI( Performance Application Programming Interface)是一組可以在多個處理器平台上對硬件性能計數器 進行訪問的標准接口,它的目標是方便用戶在程序運行時監測和采集由硬件性能計數器記錄的處理器事件 信息。

不同的處理器會根據自身的體系結構特征定義出不同的處理器事件集合,在 PAPI 中這些 事件被稱為原生事件(Native Event)。同時,不同的處理器也會具有不同數量的硬件性能計數器,而在 任意時刻一個計數器只能對一個指定的原生事件進行監測。考慮到事件監測和性能分析的需求,不同處理 器的原生事件集合往往在功能上會有交集(例如那些和存儲層次訪問、Cache 一致性協議、周期和指令計 數、功能單元和流水線狀態等方面相關的事件),但是其對應的原生事件名稱卻未必相同。為了便於事件 甄別,PAPI 將這些在不同處理器中存在功能共性的原生事件抽象成了 PAPI 接口專用的預制事件 (Preset Event)並統一命名。PAPI 預制事件不僅僅是對單一原生事件的簡單映射,根據不同體系結構 中原生事件設定的差異,它也可能是由多個原生事件構成,例如記錄 L1 Cache 失效次數的 PAPI 預制事 件,在某些處理器上實現時就需要依賴 L1 D-Cache 失效次數和 L1 I-Cache 失效次數等兩個原生事件的 支持。通過定義預制事件,PAPI 接口具有了一定的可移植性,但是對於某些處理器中定義的原生事件集 合,PAPI 預制事件可能無法將其完全覆蓋。

PAPI 提供了兩類接口訪問硬件性能計數器:一類是 比較簡單的高層接口用於完成基本的計數測量,另一類是可編程的底層接口能夠滿足用戶的復雜的監測需 求。

PAPI 高層接口提供了一些訪問硬件性能計數器所需的基本功能,例如配置計數器、啟動計數、停止計 數、讀取計數器的數值等。高層接口只能利用 PAPI 預制事件,而不能夠通過配置計數器去監測超出預制 事件覆蓋范圍以外的處理器原生事件。不過,PAPI 高層接口能夠直接返回在程序測評中最經常使用的一 些性能指標,例如每個周期執行完成的指令數、每秒執行完成的浮點指令 / 浮點操作數、程序的運行時 間等;另外,高層接口還能獲取一些系統信息,例如處理器能夠支持的硬件性能計數器的個數等。

不同於高層接口只能使用 PAPI 預制事件,PAPI 底層接口能夠直接使用原生事件對程序運行時的 處理器硬件行為進行監測。用戶可以將一個或多個原生事件組成一個事件組(Event Set),然後通過設 置硬件性能計數器對事件組中所有的原生事件同時進行監測,進而根據監測結果分析程序的性能問題,例 如通過同時采集每秒執行完成的浮點指令數和 L1 Cache 失效次數就有助於分析是否是因為 L1 Cache 的 命中率不高導致了程序浮點性能的下降。需要注意的是,事件組中的原生事件個數不能夠超過處理器所能 支持的硬件性能計數器個數。

與高層接口相比,PAPI 底層接口的使用更加靈活,能夠對處理器事 件進行更全面的監測。因此,本文的主要內容是探討如何利用 PAPI 底層接口監測和采集 Java 程序的硬 件執行特征。

常用的 PAPI 底層接口

當前的 PAPI 實現有 C 語言和 Fortran 語言兩個版 本。本文以 C 語言版本為例,在表 1 中列出了用戶在對程序運行時產生的事件進行監測時常用的底層接 口。

表 1. 常用的 PAPI 底層接口

PAPI 底層接口 接口功能簡 述 PAPI_library_init 初始化 PAPI 接口庫 PAPI_create_eventset 創建事件組 PAPI_add_event / PAPI_add_events 向事件組中添加原生事件或者 PAPI 預制事件 PAPI_remove_event / PAPI_remove_events 從事件組中刪除事件 PAPI_start 啟動計數器對事件組的計數 PAPI_read 讀取計數器數值 PAPI_stop 停止計 數器計數並讀取當前的計數器數值 PAPI_cleanup_eventset 清除事件組中的 事件 PAPI_destroy_eventset 銷毀事件組 PAPI_shutdown 終止使用 PAPI 並釋放所有相關資源

清單 1 給出的代碼片段簡要地示意了利用 PAPI 接口監測程序的硬件執行特 征的工作流程,該代碼片段利用了表 1 中所列的常用的 PAPI 底層接口。

清單 1. 利用 PAPI 接 口監測程序的硬件執行特征

#include <papi.h>
#include  <stdio.h>

main() {

  int EventSet;
  long_long  values[1], values1[1], values2[1];

  /* Initialize the PAPI library  */
  if (PAPI_library_init(PAPI_VER_CURRENT) != PAPI_VER_CURRENT)
     handle_error();

  /* Create an EventSet */
  EventSet =  PAPI_NULL;
  if (PAPI_create_eventset(&EventSet) != PAPI_OK)
     handle_error();

  /* Add an event about Total Instructions Executed  (PAPI_TOT_INS) to EventSet */
  if (PAPI_add_event(EventSet, PAPI_TOT_INS) !=  PAPI_OK)
    handle_error();

  /* Start counting events */
   if (PAPI_start(EventSet) != PAPI_OK)
    handle_error();

  /*  Read counters before workload running*/
  if (PAPI_read(EventSet, values1) !=  PAPI_OK)
    handle_error();

  /* Do some computation here  */

  /* Stop counting events */
  if (PAPI_stop(EventSet, values2)  != PAPI_OK)
    handle_error();

  /* Get value */
   values[0] = values2[0] – values1[0];

  /* Clean up EventSet */
  if (PAPI_cleanup_eventset(EventSet) != PAPI_OK)
    handle_error();

  /* Destroy the EventSet */
  if (PAPI_destroy_eventset (&EventSet) != PAPI_OK)
    handle_error();

  /* Shutdown PAPI  */
  PAPI_shutdown();

}

如清單 1 所示,PAPI 接口庫要在經過 初始化後才能被使用,不過這在使用 PAPI 高層接口時並不用被給予更多的關注,因為高層接口會自動地 隱式執行接口庫的初始化過程。但是用戶在使用 PAPI 底層接口時,就必須首先顯式地調用一次 PAPI_library_init 函數或者在此之前通過調用高層接口來完成 PAPI 接口庫的初始化。

在初始 化 PAPI 接口庫之後,用戶需要調用 PAPI_create_eventset 函數創建一個內容為空的事件組(注意:用 於指示事件組地址的變量必須首先被初始化為 PAPI_NULL),然後根據自己的需要向其中添加事件以被硬 件性能計數器監測和計數。PAPI 底層接口中用於向事件組中添加事件的函數是 PAPI_add_event 或者 PAPI_add_events,它們的區別在於前者每次只是添加單個事件,而後者則能夠通過一個事件數組一次性 完成事件添加。PAPI 底層接口可以使用的事件既包括 PAPI 預制事件也包括處理器原生事件,不過在添 加原生事件時用戶需要對相關處理器的設計具有全面深入的了解以保證計數器的使用正確,例如在某些處 理器中一些原生事件只能由特定的計數器監測。在添加事件的過程中,用戶可以使用 PAPI_remove_event 或者 PAPI_remove_events 等函數刪除之前已經被添加的事件。類似的,這兩個事件刪除函數的區別也在 於前者是刪除單個事件而後者是刪除一個事件數組。

完成了事件添加,用戶就可以調用 PAPI_start 函數啟動硬件性能計數器對事件組中的各個事件的計數。與此相對應的是 PAPI_stop 函數, 該函數在停止計數器計數的同時還會讀取計數停止時刻的計數器數值。在硬件性能計數器對事件進行監測 和計數的過程中,用戶可以隨時調用 PAPI_read 函數讀取計數器當前記錄的事件發生次數。如清單 1 所 示,為了獲得被監測程序的運行時硬件執行特征,用戶需要在被監測程序執行前調用一次 PAPI_read 函 數,然後再在其執行完畢後調用一次 PAPI_read 函數(清單 1 中通過調用 PAPI_stop 函數完成被監測 程序執行完畢後的計數器讀取操作)。最終,兩次采得的計數器數值之間的差值就是被監測程序運行過程 中產生的相關的處理器事件數。

在利用 PAPI 接口完成對被監測程序的硬件執行特征的監測後, 用戶需要進行一些收尾和整理工作,例如清單 1 先後調用了 PAPI_cleanup_eventset 和 PAPI_destroy_eventset 等函數,清除了事件組中的事件並釋放了相關事件組占用的內存資源,最後再調 用 PAPI_shutdown 函數終止對 PAPI 的使用。

PAPI 的安裝與使用

PAPI 接口功能的實現 依賴於底層的處理器和操作系統支持,因此它的安裝過程在不同的系統中存在著較大差異:在有的系統中 可以直接按照最普通的步驟,在 PAPI 源文件目錄下先後執行 configure 和 make 命令即可完成安裝; 而在有的系統中就需要略微復雜的步驟。PAPI 項目開發者特別在 PAPI 源文件的根目錄下提供了描述 PAPI 安裝過程的說明文件 INSTALL.txt,其中針對不同的系統配置都給出了相應的安裝提示。以在處理 器為 IBM POWER5+、操作系統為 Linux 的系統上安裝 PAPI 為例,用戶就必須首先為操作系統打上相關 的補丁(例如 perfctr 或者 perfmon2),以支持 PAPI 接口對處理器硬件性能計數器的訪問。

在使用 PAPI 接口時,用戶需要首先將 libpapi 共享庫所在的路徑加入到系統的環境變量 LD_LIBRARY_PATH 中,同時還需要將 PAPI 源文件所在的路徑加入到系統的環境變量 PAPI_EVENTFILE_PATH 中。完成環境變量設置後,用戶在使用 gcc 編譯調用了 PAPI 接口函數的應用程 序時,需要加入編譯選項 -lpapi,使得程序可以動態鏈接到相應的函數實現。

利用 PAPI 接口監 測 Java 程序

Java 程序的執行實體是線程,特別是多線程的 Java 程序在當前更有著廣泛的應用 。因為同一 Java 程序的線程共享處理器的硬件性能計數器,線程的交替執行會導致事件計數的混亂,所 以如果用戶要對 Java 程序的硬件執行特征進行監測就必須以 Java 線程為粒度,只有這樣才能准確記錄 下每個線程的真實數據。因此,在利用 PAPI 接口監測 Java 程序的硬件執行特征時,除了前文表 1 列 出的 PAPI 底層接口,用戶還需要用到一些和程序線程監測相關的接口,如表 2 所示。

表 2. 與 程序線程監測相關的 PAPI 底層接口

PAPI 底層接口 接口功能簡述 PAPI_thread_init 初始化 PAPI 接口庫對線程監測的支持 PAPI_thread_id 獲得當前線程的線程標識符 PAPI_attach 將一個事件組綁定到指定線程上 PAPI_detach 取消事件組與相關線程之間的綁定

為了獲得 PAPI 對程序線程監測的支持,用戶需要在初始化 PAPI 接口庫之 後調用 PAPI_thread_init 函數。在被監測線程創建後,用戶通過調用 PAPI_thread_id 可以獲得線程的 標識符,並利用 PAPI_attach 函數將一個已經創建好的事件組和該標識符代表的線程進行綁定,這樣可 以保證在該事件組上進行監測所得的事件計數都是由該線程在運行時產生的,從而准確地獲得單個線程的 硬件執行特征而消除了其它線程的干擾。在線程監測完成後,用戶需要通過調用 PAPI_detach 函數釋放 相關的資源。

用戶在利用 C 語言實現的 PAPI 接口監測 Java 程序運行過程中的硬件執行特征時 ,最容易想到的做法就是通過 JNI 接口在 Java 程序中調用本地語言編寫的接口和函數。PAPI 的開發者 也試圖為用戶提供這樣的方便,在源文件的 jni 目錄下提供了本地方法的封裝,但是這部分工作並沒有 完成,相關代碼沒有經過測試並且可能無法使用。另外,JNI 方法需要用戶改寫被監測 Java 程序的源代 碼,這提高了 PAPI 使用的復雜度。針對於此,本文提出了一種更便捷的基於 Java Virtual Machine Tool Interface(JVMTI)的方法利用 PAPI。

JVMTI 提供了一種編程接口,允許用戶創建代理程 序(Agent)以監測和控制虛擬機和 Java 應用程序。代理程序可以向運行時的虛擬機訂閱其感興趣的事 件,例如虛擬機和 Java 應用程序執行狀態的改變等等。這些虛擬機事件在其發生時會以調用事件回調函 數的方式激活代理程序並被回調函數進行相應的處理。代理程序可以使用任何支持 C 語言標准的本地語 言編寫,它以動態鏈接庫的方式存在並在 Java 應用程序啟動時被加載。

采用基於 JVMTI 的方法 利用 PAPI 接口監測 Java 程序的硬件執行特征,在設計代理程序時至少需要關注虛擬機啟動、虛擬機停 止、應用線程啟動、應用線程結束等虛擬機事件並在事件對應的回調函數中調用相關的 PAPI 接口函數。 一般的,在實現過程中,各個虛擬機事件對應的回調函數功能如表 3 所示。

表 3. 虛擬機事件對 應的回調函數功能

虛擬機事件 回調函數中調用的 PAPI 函數 虛擬機啟動 PAPI_library_init
PAPI_thread_init 虛擬機停止 PAPI_shutdown 應用線 程啟動 PAPI_create_eventset
PAPI_add_event / PAPI_add_events
PAPI_remove_event / PAPI_remove_events
PAPI_attach
PAPI_start
PAPI_read 應用線程結束 PAPI_stop
PAPI_detach
PAPI_cleanup_eventset
PAPI_destroy_eventset

如表 3 所示,創建 、綁定事件集和向其中添加事件都在每個線程啟動事件的回調函數中進行,這樣每個線程就可以擁有自己 獨立的事件集資源。同時,代理程序需要借助一個全局變量,記錄下各個線程對應的事件集,以備在不同 的虛擬機事件回調函數(主要是應用線程啟動和應用線程結束)中被相關的 PAPI 接口函數調用。

在本文附帶的樣例代碼中,實現了一個在處理器為 IBM POWER5+、操作系統為 Linux 的系統中, 使用基於 JVMTI 的方法調用 PAPI 接口並對 Java 程序的硬件執行特征進行監測的例子。其中,被監測 的 Java 程序創建了一個子線程計算 Fibonacci 數列,而用 C 語言編寫的代理程序則負責 JVMTI 所需 的代理初始化以及實現表 3 中所列的各個虛擬機事件對應的回調函數。

為了方便用戶區分那些被 監測到的從屬於不同 Java 線程的硬件計數器數值,本文附帶的樣例代碼利用 JVMTI 接口提供的 GetThreadInfo 方法獲得線程名稱,如清單 2 所示。

清單 2. 利用 JVMTI 接口獲得線程名稱

void 
printThreadName(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread  thread)
{
  jvmtiThreadInfo info;
  jvmtiError err;

  if  (thread == NULL) {
    printf("Warning: NULL thread\n");
     return;
  }

  err = (*jvmti_env)->GetThreadInfo(jvmti_env, thread,  &info);
  if (err != JVMTI_ERROR_NONE) {
    printf("Error:  GetThreadInfo, Error code = %d\n", err);
    return;
  }

   printf("Thread name: %s\n", (char *)info.name);

  return;
}

在設置被監測的處理器事件時,用戶需要查閱相關處理器的手冊以獲得更詳盡的信息。以 IBM POWER 處理器為例,其生產廠商提供了多個事件簇(Event Group)給用戶使用,其中每一個事件簇 都是若干處理器原生事件的集合。一個事件簇中包含的事件個數與處理器擁有的硬件性能計數器個數相同 ,這些事件在計數器的使用上不存在沖突,它們將在程序運行時同時被各自對應的計數器監測。這些事件 的產生往往存在著功能或者邏輯上的關聯,以事件簇的方式對它們同時進行監測有助於用戶對程序運行過 程中的硬件行為獲得更全面的認識。在本文附帶的樣例代碼中,被添加到事件組中的被監測事件就同屬於 IBM POWER5+ 的一個事件簇,其具體描述如表 4 所示。

表 4. 樣例代碼所用 IBM POWER5+ 處理 器事件的描述

計數器 事件名稱 事件編碼 事件簡述 0 PM_1PLUS_PPC_CMPL 0x40000002 有至少一條 PPC 指令 被執行完成的周期 1 PM_GCT_EMPTY_CYC 0x4000017A 處理 器流水線為空的周期 2 PM_GRP_CMPL 0x400001AB 有指令 組被執行完成的周期 3 PM_CYC 0x40000011 處理器周期 4 PM_RUN_INST_CMPL 0x400001E2 有指令被處理器執行完 成 5 PM_RUN_CYC 0x40000138 處理器處於 RUN 狀態的周 期

如表 4 所示,在 IBM POWER5+ 處理器中,共有六個硬件性能計數器,因 此一共可以有六個處理器事件被同時監測。不過,六個計數器中的 0 號到 3 號計數器是可編程的,可以 監測多種不同的事件;而 4 號和 5 號計數器則是專用計數器,只能夠對應地監測 PM_RUN_INST_CMPL 和 PM_RUN_CYC 事件。通過在程序運行過程中對這個事件簇中的事件發生次數進行監測,用戶能夠獲得程序 的 CPI(Cycles per Instruction)、流水線正常運行的周期比例、流水線為空的周期比例等性能指標。

以本文附帶的樣例代碼為例,利用 PAPI 接口監測 Java 程序的硬件執行特征,用戶首先要按照 常規方法編譯被監測的 Java 應用程序。

javac Fibonacci.java 

然後, 將 C 語言編寫的代理程序編譯為動態鏈接庫。

gcc -shared -lpapi -lpthread -o  libPAPI_Agent.so -I $JAVA_HOME/include/ PAPI_Agent.c 

注意,因為本文在代理 程序中使用了 pthread 庫函數,所以要在編譯選項中增加 -lpthread 參數。另外,還需要保證在編譯時 已經包含了定義有 JVMTI 接口函數的頭文件。

最後,在執行被監測 Java 程序的時候,通過下面 的指令加載代理程序並對 Java 程序進行運行時的監測。

java -agentlib:PAPI_Agent  Fibonacci 

在 Java 程序的運行過程中,JVMTI 代理程序將對表 3 所列的虛擬機事件進 行監控並調用與之相對應的回調函數。回調函數通過 PAPI 接口訪問處理器的硬件性能計數器,並在被監 測程序運行完成時獲得程序中各個線程的硬件執行特征,其輸出如下。

Thread name:  main 
Counter value[0] = [22140488]
Counter value[1] = [6640502]
Counter value[2] = [22418555]
Counter value[3] = [70891030]
Counter  value[4] = [57302729]
Counter value[5] = [70891030]
Thread name:  DestroyJavaVM helper thread 
Counter value[0] = [2168]
Counter value[1] =  [4504]
Counter value[2] = [2183]
Counter value[3] = [14284]
Counter  value[4] = [5774]
Counter value[5] = [14284]
Thread name: Thread-1
Counter value[0] = [7259]
Counter value[1] = [7774]
Counter value[2] =  [7367]
Counter value[3] = [51694]
Counter value[4] = [20038]
Counter  value[5] = [51694]

通過分析上面的輸出信息,用戶可以發現在被監測的 Java 程序運 行過程中有三個線程先後被啟動,其中名為 Thread-1 的線程是用於進行 Fibonacci 數列計算的應用線 程。根據表 4 的處理器事件描述,用戶可以算得該應用線程的 CPI 為 2.58,在程序運行過程中在 14% 的時間裡流水線工作正常,而在另外 15% 的時間裡流水線為空載。如果對其它更多的處理器事件進行監 測,用戶就可以獲得更全面的程序運行時的硬件執行特征進而開展更深入的分析,以發現程序性能問題的 根源所在。關於如何利用程序的硬件執行特征分析程序性能,請參閱參考資源中所列的相關網頁鏈接。

總結

PAPI 是一組用於訪問處理器硬件性能計數器的本地接口,用戶可以利用它獲得程序 運行過程中產生的各種處理器事件的發生次數,進而將這些數據作為評估和分析程序性能的可靠依據,有 助於更准確地發現程序性能問題的根源。本文在深入分析 PAPI 接口功能的基礎上,結合 Java 程序的執 行特征,提出了一種基於 JVMTI 的方法,該方法能夠有效地利用 PAPI 接口對 Java 線程的運行時硬件 特征進行監測。

隨文源碼:http://www.bianceng.net/java/201212/698.htm

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved