薛子涵, 解 達, 宋 威
1(中國科學院 信息工程研究所, 北京 100093)
2(中國科學院大學 網絡空間安全學院, 北京 100049)
現代處理器中一般都擁有若干被稱為硬件性能計數器(Hardware Performance Counter, HPC)的寄存器[1].這種寄存器只會按照軟件的配置在指定的事件發生時自增計數, 對處理器的正常工作或者性能表現不會有影響.這些被記錄的信息能夠幫助軟件工程師對代碼進行優化, 也能夠協助計算機架構師對處理器結構進行調優, 因此HPC幾乎成為所有現代處理器的標配.作為近年來備受學術界和工業界關注的開源指令集RISC-V, 其處理器在設計之初就把HPC考慮了進去并為其制定了相關標準.RISC-V特權指令規范[2]定義了若干控制和狀態寄存器(Control and Status Registers,CSRs)作為計數器和選擇器.其中選擇器將一個或多個(微)體系結構事件的信號接入計數器, 從而構成了可捕獲特定事件的HPC.在現代處理器結構愈發復雜的時代, 這種將HPC集中在CSR模塊內的方式不利于HPC的拓展, 也限制了可同時監測的體系結構事件數量.為此, 本文設計了一種分布式的性能計數器.按照處理器內部模塊的劃分, 將HPC分布在不同區域,并通過片上互連將HPC和CSR模塊連接, 從而降低了CSR模塊復雜度, 同時也為HPC的拓展提供了便利.
本文組織架構如下: 第2節介紹了相關的背景知識.第3節介紹了SiFive U74-MC[3]的性能計數器并分析其不足之處.第4節針對現有的HPC方案存在的問題, 提出了分布式的HPC方案.第5節完成了將分布式的HPC部署到lowRISC-v0.4[4], 通過運行SPEC CPU2006[5]分析結果并評估該方案效果.第6節總結本文.
RISC-V[6]是一種優秀的開源指令集架構, 由加州大學伯克利分校于2010年發布.RISC-V充分吸取了其他指令集優點: 結構優雅便于實現、靈活性強方便拓展、免費開源無須高昂的授權費用、社區活躍工具鏈完善等[7,8].這些優點使其得到了眾多科研團隊和商業公司的青睞.目前已有平頭哥、SiFive在內的商業公司推出了基于RISC-V指令集的處理器.然而RISC-V定義的HPC存在不足, 無法同時監測多種體系結構事件, 在使用中局限性較大.
RocketChip[9]是加州大學開源的基于RISC-V的64位處理器.其中RocketChip的Rocket處理器核采用5級標量順序流水線, 采用Chisel (Constructing hardware in a Scala embedded language)語言開發[10].作為加州大學為推廣RISC-V而開發的開源處理器,RocketChip為全世界的科研團隊提供了針對體系結構研究的優良平臺.
lowRISC-v0.4是劍橋大學開發的基于RocketChip的開源SoC, 其基本信息如表1所示[4].相比于其他開源SoC 項目lowRISC-v0.4加入的OSD (Open SoC Debug) 模塊[11]提供了函數追蹤、特權模式監測、程序下載(通過UART接口)等功能.這些功能在代碼調試過程中提供了詳細的調試信息.本文利用OSD模塊與上位機結合的方式, 使得lowRISC-v0.4能夠自動運行SPEC CPU2006 benchmark[5], 并在運行結束后讀取HPC, 獲取體系結構事件的統計值, 從而驗證HPC 能否正常工作.

表1 lowRISC-v0.4基本信息
硬件性能計數器是一組能夠記錄(微)體系結構事件發生次數的寄存器.這些事件通常包括時鐘周期數、已執行指令數、分支預測失敗次數、各級緩存(cache)的缺失/命中次數、TLB (Translation Look-aside Buffer)的缺失/命中次數等[1].對于會造成流水線阻塞的事件,某些處理器中的HPC還會記錄該事件導致的阻塞周期數[13,14].這些寄存器通常作為系統寄存器(比如ARM中的System Register, RISC-V中的CSR), 從而能夠被指令直接訪問.盡管HPC不是處理器正常工作的必要組成單元, 也不會對處理器性能有影響, 然而HPC提供了硬件運行過程中實時的狀態信息.利用這些信息我們能夠更加高效地對系統狀態進行監測[1]、對硬件資源進行高效利用[15]、對功耗進行合理管理[16]、對惡意代碼進行有針對性的檢測[17,18]、對計算機系統結構[19]進行優化.因此, 幾乎所有現代處理器都會配置該計數器.HPC的管理和配置工作通常由性能監測單元(Performance Monitor Unit, PMU)完成.PMU為每一個HPC都分配了一個事件類型選擇寄存器, 當(微)體系結構中發生的事件和該寄存器選中的事件匹配時, PMU就會控制對應的HPC增加計數值.除了計數器和選擇寄存器這兩個成對出現的寄存器之外, 某些處理中的PMU還包含了其他寄存器, 從而可以提供更豐富、更強大的額外功能: 比如訪問控制、特權級過濾(如過濾用戶態下的事件)等.HPC的基本配置和訪問較為簡單, 通常只需在選擇寄存器中, 選中要監控的事件.對于較為復雜的需要則可以直接使用Linux下的開源工具—perf[20].經過多年的發展, 商業處理器HPC的硬件結構和軟件配套已經十分完善, 能夠滿足各種應用需求.然而, 迄今為止開源RISC-V處理器(如RocketChip, lowRISC)中的HPC存在功能簡陋、拓展性弱、能夠同時統計的體系結構事件少等不足, 無法滿足對于體系結構的研究需求.基于此本文設計、實現了一種分布式HPC,并使用該HPC進行我們相關的體系結構研究工作.
U74-MC[3]是由SiFive公司發布的基于RISC-V指令集的64位高性能多核處理器, 可應用于數據中心、通信基站等對性能要求較高的場景.作為目前性能最優的RISC-V處理器之一, U74-MC擁有功能全面的HPC.因此本文對U74-MC的HPC進行了著重分析.U74-MC的HPC可以分為兩類: 第1類按照RISC-V特權指令規范, 將若干CSR作為HPC寄存器, 軟件可以通過CSR指令配置和訪問這些HPC.第2類專門用來統計L2的微體系結構事件, 這類HPC不會占用CSR,而是像外設一樣通過MMIO (Memory-Mapped I/O,即內存映射)與處理器核連接.為了便于區分, 本文把第1類稱為核內HPC, 第2類稱為外設HPC.
按照RISC-V特權指令規范[3], U74-MC分配了4個CSR作為HPC (這些計數器稱為mhpmcounter),其中兩個(mcycle和minstret)用于統計時鐘周期和已執行指令數, 其余兩個是可編程的CSR, 可用于夠捕獲選定的事件, 并完成計數.事件的選擇由mphevent寄存器控制, 能夠選中某一種或某幾種事件.為了安全起見,用戶級(user-level)的程序不能配置該寄存器, 也無法直接訪問mhpmcounter, 只能在機器級(machine-level).程序在[m|s]counteren中開啟了對應的計數器訪問權限后, 用戶級程序才被允許通過訪問hpmcounter, 從而間接獲取mhpmcounter中的存儲的計數值.即hpmcounter是mhpmcounter的影子寄存器(shadow CSR).
如圖1所示, 核內HPC的寄存器位于CSR模塊內部, 事件信號來自不同模塊.因此這種結構的HPC擁有極低的訪問延時, 軟件通過CSR命令能夠直接獲取HPC數據.雖然核內HPC結構簡單, 便于實現且能夠滿足大部分應用場景, 但仍存在不足: 首先, 無法同時監測大量事件, 當監測事件大于核內HPC數量時,只能由軟件不斷切換監測的事件類型.這種方式不僅損害了性能, 同時還降低了測量精度.其次, 計數器都集中在CSR區域, 而(微)體系結構事件卻分布在如處理器核、緩存、直接存儲器訪問(Direct Memory Access,DMA)等各個模塊中.繁多的計數信號來源給前端設計和后端布線帶來了巨大挑戰.

圖1 核內HPC結構
現代處理器內部擁有種類眾多的模塊: 處理器核、緩存、DMA等, 將這些模塊的事件信息全部匯總在核內HPC是很不明智的做法.將同一模塊的HPC集中在一起并作為外設連接到MMIO, 這樣就避免了核內HPC計數信號過于分散的問題, 而且通過訪存指令(LOAD/STORE)就能對這些HPC進行控制和訪問.U74-MC的L2 HPC就使用了這種設計方來案監測L2緩存的(微)體系結構事件.
如圖2所示, 外設HPC就像外設一樣通過MMIO連接至處理器核, 由于IO空間范圍較大, 此結構具有較好的拓展性.外設HPC彌補了核內HPC缺陷, 能夠同時監測更多的(微)體系結構事件.但仍有一些不足:首先, IO空間無法對用戶態下的程序直接可見, 訪問前需要經過操作系統的映射, 從而產生不小的代價.其次,MMIO由諸多外設共享, HPC數據可能被惡意外設獲取, 進而被利用發起側信道攻擊, 引起難以監測的安全問題.最后, 相比于CSR訪問命令, LOAD/STORE指令影響L1D (一級數據緩存)的缺失率, 即訪問HPC會影響測量的準確性.

圖2 外設HPC結構
針對以上不足, 本文提出一種分布式的性能計數器方案, 該方案充分結合了核內/外設 HPC兩者優點:首先, 該結構擁有眾多HPC, 能同時監測上百種事件.其次, 易于使用, 利用3個CSR就夠控制和訪問這些HPC, 不影響L1D性能有更好的準確性, 不占用MMIO,因此不會帶來地址映射的額外開銷.最后, 有著更好的安全特性, 能夠通過CSR的權限控制管理HPC的訪問, 從而避免了HPC被非法使用, 進而導致側信道攻擊.
核內HPC結構模型中的每一個計數器都占據了1個CSR, 當需要同時監測更多事件時, 就需要更多的CSR.如果將這些計數器從CSR中剝離出來, 根據其事件類型和觸發信號的來源, 將其分布在不同模塊中, 并通過片上互連, 將各個計數器和對應的CSR相連, 這樣就可以通過使用少量的CSR來控制和訪問數量龐大的HPC, 按照這種策略, 本文設計了如圖3所示的分布式性能計數器.從圖3中可以清晰地看出它由3個重要部分組成: HPCManager, HPCClient和HPCInterconnect.而HPC的寄存器被分散在了HPCManager中.

圖3 分布式性能計數器結構
HPCManager主要負責接收和處理來自HPCClient的數據請求, 根據請求命令去查找指定的HPC, 并把該HPC的數據返回給HPCClient.1個HPCManager最多可以容納64個HPC, 這些HPC并無編程功能, 每個計數器只能夠記錄特定的(微)體系結構事件.HPC按照所屬模塊進行劃分, 比如處理器核中的事件統一劃分給一個HPCManager, L2 (二級緩存)分片中的事件統一劃分給另一個HPCManager.當模塊中的事件超過64種時, 可為該模塊分配多個HPCManagerID.顯然, 相比于計數器集中在CSR中的方式, 這種按模塊去劃分的分布式方案拓展性更強, 擁有更規整、更清晰的結構, 同時帶給后端布線的壓力也更小.
HPCClient主要負責向HPCManager發起HPC數據請求以及接收返回的數據, 其結構如圖4所示.可以看出, HPCClient位于處理器核中的CSR模塊, 它占用了3個CSR: hpcm (hardware performance counters bitmap),hpcc (hardware performance counters config)以及hpcr(hardware performance counters receive).通過這3個CSR, 軟件能夠獲取到任意HPC的數據.它們的詳細內容將在本文的4.2小節展開.

圖4 HPCClient結構
HPCInterconnect則把HPCClient和HPCManager連接在一起, 并且把這兩種模塊的命令、數據傳遞到指定位置.HPCInterconnect采用循環優先級, 它保證了在多核結構中, 每個核對HPCManager都擁有相同的優先級, 在多核同時向同一HPCManager發起請求時,可以有效避免“餓死”的情況.
在CSR中定義了3個寄存器, 即hpcc, hpcm, hpcr,通過這3個寄存器來完成對HPC的管理和訪問.
hpcc 即 hardware performance counters config.其比特位分配情況如圖5所示.作為硬件性能計數器的功能配置寄存器, 該CSR功能包括: 向指定的HPCManager發起HPC請求、指示接收狀態、指示軟件讀取是否失敗等.
以下內容是依據圖5對hpcc的功能劃分進行說明:

圖5 hpcc寄存器
(1) trigger: hpcc[0] ReadWrite
該位主要功能是發起和取消HPC請求.如果此位為0, 則表示當前沒有正在進行的HPC請求, 這時寫入1就可以觸發一次新的HPC請求.若此位為1, 則表示有正在進行的HPC請求, 軟件可以通過寫入0的方式來撤銷當前未完成的請求.當HPCClient接收完所有指定的數據后, trigger會自動復位, 表示完成了1次請求, 即所有需要的HPC數據均已存入fifo.
(2) interrupted: hpcc[1] ReadOnly
該位用于表示HPC在請求過程中, 是否發生過上下文切換.當軟件寫hpcm時, 該位自動復位, 當上下文切換時, interrupted自動置位, 表示HPC請求階段可能出現數據錯誤, 需要重新發起請求.軟件只擁有該位的讀取權限.
(3) empty: hpcc[2] ReadOnly
該位用于表示接收返回數據的fifo狀態.當fifo空時, 該位會被置位, 否則被復位.當軟件寫hpcm時該位自動置位, 軟件只擁有該位的讀取權限.利用empty, 我們可以實現更快速地HPC讀取, 當empty復位后, 軟件就可以開始讀取該數值, 而不必等到接收完HPCManager返回的所有數據(此時trigger自動復位)才開始讀取工作.因而把該位當作讀取開始的判斷依據, 能夠節約一定的時鐘周期.
(4) readerror: hpcc[3] ReadOnly
該位用于表示軟件讀取HPC時是否發生了錯誤.當軟件寫hpcm時, 該位自動復位, 當軟件從空的fifo中讀出了數據時, 該位置位, 表示讀到了錯誤的數據.軟件只擁有該位的讀取權限.
(5) HPCMangerID: hpcc[20:4] ReadWrite
即HPCManager的ID.軟件向該位寫入正確的ID號, 從而在選中預期的HPCManager之后, 置位trigger就能向該HPCManager發起HPC請求.為了增強擴展性, 該位無法獨立工作, 需要與hpcm寄存器配合使用.事實上, HPC可以按照任意規則劃分到HPCManager,本著“一種邏輯清晰的劃分規則, 既能給使用者帶來便利也能夠減輕后端布線的壓力”的思想, 本文采取按照“模塊化”的劃分策略, 將HPCMangerID分割成module和extend兩部分, module指的是處理器核0, 處理器核1, 末級緩存分片0, 末級緩存分片1等模塊.而復雜模塊內的事件數量通常會超過64, 因此會使用extend對模塊分配多個HPCManagerID.
useren: hpcc[21] ReadWrite
該位用于對用戶級程序HPC訪問權限的控制, 用戶級的程序對該位只有讀取的權限, 只有機器級或特權級下的程序能夠對該位進行更改.若此位為1, 則用戶級的程序能夠發起HPC請求, 否則用戶級的程序在置位trigger時會觸發異常.該位的置位復位可以由操作系統完成, 這樣能夠對任意進程進行HPC訪問權限的控制.避免了HPC數據被惡意程序利用, 有著很好的安全特性.
hpcm即 hardware performance counters bitmap.作為硬件性能計數器的位圖選擇器, 該寄存器的每一比特都對應HPCManager中的1個HPC.當hpcc.trigger處于復位狀態時, 軟件擁有hpcm的寫權限, 否則軟件只能讀取該寄存器.當 HPCClient按照HPCMangerID指示向目標HPCManager發起請求時, hpcm選定了要讀取的HPC, 同時該寄存器自動復位.之后每接收到HPCManager返回的1個HPC, 就將對應的比特位置位, 通過讀取該位, 軟件可以知曉已接收到的HPC.受限于寄存器最大位寬, 因此每次選取的HPC不能超過64個, 從而理論上, 1個HPCManager最多管理64個計數器.為不同模塊分配單獨的HPCManager, 記錄了不同模塊內部的(微)體系結構事件, 目前支持的事件和對應的hpcm位圖如表2所示.read表示讀取緩存的次數.readmiss表示讀取緩存, 但發生數據缺失的次數.write表示向緩存寫入數據的次數.writemiss表示向緩存寫入數據, 但是發生缺失的次數.writeback表示將緩存中的數據寫入下一級緩存或者內存中的次數.

表2 記錄的事件和其所在位置
hpcr作為硬件性能計數器的接收寄存器, 軟件只有該寄存器的讀取權限.通過讀取該寄存器, 軟件可以獲得HPC數值.hpcr是直接連接到fifo輸出端口, 軟件每讀取一次hpcr, fifo就會彈出一個新的數據存入hpcr寄存器.當fifo為空時就會把過時或錯誤的數據存入該寄存器.發起HPC請求后, 軟件可以在hpcc.trigger由1變0后才開始讀取hpcr (表示當前的HPC全部接收完畢), 也可以在hpcc.empty由1變0后立刻讀取(表示當前接收到有返回的HPC).在沒有異?;蚶獾那闆r下, 兩者得到的數值以及數值順序完全相同, 且后者更節約時鐘周期, 尤其在請求的HPC數量較多的情況中.
在裸機環境(bare metal environment)中, 不存在多個線程對HPC控制權的爭搶問題, 配置并訪問HPC十分簡單.算法1適用于裸機環境下獲取HPC數值:行①使用CSR置位指令, 將bitmap寫入hpcm寄存器,其中bitmap代表選中HPCManager中的某幾個HPC.行② id用來選中HPCManager, 置位trigger會觸發對HPCManager的請求.行④等待HPCManager返回數據(empty復位).行⑤empty復位后, 軟件讀取hpcr并將得到的HPC數據存入數組.

算法1.裸機環境HPC的訪問輸入: HPCM 的id和HPC對應的bitmap輸出: HPC記錄的數據Function get_HPCs(id, bitmap):① set_csr(hpcm, bitmap);② set_csr(hpcc, id&trigger);③ for(i=0; i<=hpcnumber; i++) {④ while(get_csr(hpcc.empty));⑤ hpcs[i]=get_csr(hpcr);⑥ }
在多線程環境下, 可能會出現同一個處理器核中的多個線程同時向HPC發起請求.HPC的獲取需要HPC的3個寄存器相互配合, 當配置工作被其他線程打斷, 則會出現意想不到的錯誤.針對這種問題, 我們通過使用“軟件鎖”, 防止多個程序同時對HPC的3個寄存器進行控制和訪問.然而“軟件鎖”不僅實現復雜而且開銷較大, 對測量精度有影響.為此, 我們通過監測hpcc.interrupted位, 來判斷HPC的請求程序是否發生過中斷.當系統中線程個數與中斷次數均較少時, 這種方式的失敗率可以忽略.這種方法編程簡單、易于實現, 不會給系統帶來過多的額外開銷, 更加適合我們的研究工作.其具體算法如算法2所示: 行②復位hpcc寄存器, 終止可能正在進行的HPC請求.行③至行⑧如算法1, 獲取HPC數據并存入數組.行⑨判斷上述操作是否出現上下文切換, 若出現過上下文切換, 則返回到行②處.

算法2.多線程中HPC的訪問輸入: HPCM 的id和HPC對應的bitmap輸出: HPC記錄的數據Function get_HPCs(id, bitmap):① do {② clear_csr(hpcc.trigger);③ set_csr(hpcm, bitmap);④ set_csr(hpcc, id&trigger);⑤ for(i=0; i<=hpcnumber; i++) {⑥ while(get_csr(hpcc.empty));⑦ hpcs[i]=get_csr(hpcr);⑧ }⑨ } while(get_csr(hpcc.interruped))
我們通過將本文設計的分布式硬件性能計數器,部署在lowRISC-v0.4上, 其消耗的資源如表3所示.可以看出本文的設計硬件開銷較小: LUTs和Registers分別占用了1.66%和6.29%.其中, HPCClient使用寄存器相對較多, 其原因是用于接收HPC數據的緩沖區容量較大.lowRISC-v0.4能夠以50 MHz的頻率工作在Genesys2開發板[12], 其工作頻率并沒有受到HPC的影響.通過使用OSD模塊, 我們實現了lowRISC-v0.4可以自動運行SPEC CPU2006的各個整數測試集,在運行完一百億條指令后, 采用算法1讀取HPC.每個基準的測試用例結果的平均值如表4所示, 可以看出L2的讀取次數等于指令緩存和數據緩存的缺失次數之和, 符合L1和L2包含關系.結合表3、表4, 我們可以得出一個結論: 分布式的HPC能夠以極少的硬件資源為代價, 實現了準確地記錄時鐘周期、執行指令數以及L1與L2各分片的讀寫次數和缺失次數.

表3 性能計數器資源消耗情況

表4 SPEC CPU2006 (整數)運行結果
表5將分布式的HPC和SiFive公司的U74-MC處理器所包含的兩類HPC進行對比: U74-MC的核內型HPC結構簡單安全性強, 但是在監測大量事件時,需要依靠軟件不斷切換監測事件, 既增加了編程代碼也降低了測量精度.U74-MC的外設型HPC將計數器部署在IO空間, 因此有很大的拓展空間, 能夠同時監測大量事件, 然而由于通過MMIO連接, HPC數據可能泄露給惡意外設, 對處理器安全有一定影響.而且外設型HPC需要LOAD/STORE指令訪問, 這些指令對L1D的缺失率有一定影響.本文設計的分布式HPC為每個事件都單獨分配了計數器, 因而測量精度得到了保障.根據事件種類將計數器部署在不同HPCManager中, 并利用獨立的HPC互連將這HPCManager和處理器核中的HPCClient進行連接, 因此分布式HPC有著很大的拓展空間和很好的安全性.除此之外該方案僅使用3個CSR就能訪問所有的HPC, 避免了對CSR資源的過多占用.這些優點給我們之后的相關研究工作帶來了極大的便利.但仍存在諸多缺陷: 首先分布式的HPC只適用于64位處理器, 通用性較差.其次精力所限, 本文提出的方案沒有經過嚴格測試無法保障穩定性, 只適合研究工作.再次相較于商業處理器提供的眾多監測事件, 本方案目前監測的事件種類少, 只統計了我們研究需要的事件.這些不足均需要后續完善.

表5 3種HPC特點對比