Arm KleidiAI 是一款具有突破性意義的軟件庫,專為提升 Arm CPU 上的人工智能 (AI) 性能而設計。在此前發布的《Arm KleidiAI 助力 AI 框架性能提升》一文中,對 KleidiAI 進行了簡要概述,并附有相關指南鏈接,其中詳細說明了在 Linux 環境中運行 KleidiAI 矩陣乘法 (matmul) 微內核的分步操作,這份指南內容詳實且極易上手。而本篇內容則將探索如何在裸機環境中運行 KleidiAI 內核,并通過測試多款 C/C++ 編譯器,以確定如何能更高效地生成代碼。
本文將介紹如何在裸機環境中運行 KleidiAI 微內核,并針對不同編譯器在不同優化級別下的表現進行基礎基準測試。文中會用到 Arm Development Studio 的相關組件,包括固定虛擬平臺 (FVP),以及 Arm Compiler for Embedded (AC6) 的授權許可。與此同時,還提供了有關如何查看編譯器已采用(或未采用)的優化的相關信息。
設置裸機項目
本文將評估的三個編譯器分別是:
Arm Compiler for Embedded,更為人熟知名稱的是 AC6
Arm GNU 工具鏈,即 GCC
新一代 Arm 嵌入式編譯器 Arm Toolchain for Embedded (ATfE)。撰寫本文時,該工具鏈還處于 Beta 測試階段
為了在裸機項目中運行 KleidiAI 內核,可參考 Kleidi 指南中的說明。本文以 Arm Development Studio 中的 C++ 示例項目為基礎進行開發:startup_Armv8-Ax1_AC6_CPP是 AC6 版本,startup_Armv8-Ax1_GCC_CPP是 GCC 版本,而 ATfE 的移植版本則包含在 ATfE 測試版下載包中。這三個編譯器對應的項目功能相同,但需要對 Makefile 和鏈接腳本進行必要的修改。
各工具鏈的修復和更改
在粘貼 Kleidi 指南中提供的代碼后,需對這三個項目進行以下簡單修改以確保正常運行:
包含 float.h 頭文件以定義 FLT_MAX
添加 KleidiAI 頭文件的 include 路徑
將架構更改為armv8.2-a+dotprod+i8mm
要運行此代碼,需要一個具備 i8mm 擴展的 Arm 核心。此擴展在 Armv8.2-A 至 Armv8.5-A 架構中為可選功能,而在后續支持高級 SIMD 指令的核心中則為必選,因此 Arm Neoverse V1 是個不錯的選擇。Arm Development Studio 提供了 Neoverse V1 固定虛擬平臺 (FVP),此處所選用的是-C cluster0.NUM_CORES=1 -C bp.secure_memory=false -C cache_state_modelled=0
啟動代碼中存在一段用于設置 SMPEN 的讀-改-寫序列,但這在 Neoverse V1 FVP 上會引發問題。由于是復用 Arm Cortex-A 的啟動代碼來適配 Neoverse 核心,因此需要進行一些修改,而在本場景中,移除該序列即可解決問題。理想情況下,應根據 Neoverse 核心的要求重新審閱啟動代碼,但就本次研究而言,確保代碼正常運行便已足夠。
添加了一些代碼,用于向矩陣中填充隨機數據。這一步可能并非必需,因為內存中原本就已填充了重復的非零模式。
此外,還需要對各個項目單獨做一些修改。示例項目主要是實現處理器的啟動,并未考慮在啟動后運行較為復雜的負載任務:
在 ATfE 項目中,RAM 大小被設為 0x80000,這個容量過小,會導致堆與棧發生沖突。不過此問題很容易解決,因為即便是 FVP 的默認配置,其提供的 RAM 也遠大于該數值。因此,我們可以在鏈接腳本中設置更大的 RAM 大小。
在 GCC 項目中,.init_array 段被分配到 0x80100000 地址,該地址過低,會與 .eh_frame 段產生沖突。移除這一地址設置即可解決問題。
至此就能成功在裸機環境中使用三款不同的工具鏈運行 KleidiAI 內核。接下來便可開展性能測試。
基準測試方法和結果
本次研究中使用了 FVP 的周期計數器來作為性能衡量指標。雖然它并非完美,但對于本次研究而言已經足夠。由于三款編譯器運行的是相同的工作負載,因此即便存在測量誤差,其誤差程度和分布位置也會保持一致。所以,作為一種性能參考指標,FVP 的周期計數完全能滿足本次研究的需求。接著,分別在 -O0、-O1、-O2 和 -O3 這四個優化級別下,對三款編譯器的周期計數進行了測量,以啟動處理器核心、設置矩陣以及執行 KleidiAI 內核:

這里有兩個值得關注的現象。首先,大部分優化效果在 -O1 級別就已顯現。在 -O2 和 -O3 級別下雖有小幅提升(其中 GCC 的提升相對更明顯),但提升幅度遠不及 -O1 級別。這并不令人驚訝,因為 KleidiAI 內核本身已通過大量手工編寫的匯編指令進行了優化,而在 Kleidi 內核外添加的代碼既簡短又簡單。本文后續會深入分析所使用的優化手段。
其次,ATfE 的表現似乎明顯快于 AC6 和 GCC。新一代 Arm 嵌入式編譯器能在與 AC6 的對比中展現出如此優勢,固然令人欣喜,但這一性能差距也促使我進行更深入的探究。
AC6 和 ATfE 的匯編器、編譯器及 C++ 庫組件均基于 LLVM 構建,兩款工具鏈的主要差異體現在鏈接器和 C 庫上(AC6 采用專有版本,ATfE 則使用開源版本)。因此,兩者之間約 20% 的性能差距讓我頗為好奇。我需要確保所有性能數據和基準測試結果都能適用于實際項目,所以必須進一步厘清 ATfE 的速度提升究竟源于何處。
深入分析
在這一部分對性能測試進行了簡化,但同時也提升了復雜度。通過只關注 -O1 優化等級,以此簡化了測試,因為大部分優化效果都體現在這一級別。與此同時,通過將代碼分為三個部分來提高分析的粒度:
啟動:所有啟動代碼,直至進入 main ()
準備:為矩陣分配內存,向矩陣填充隨機數據
執行:運行 Kleidi 內核
周期計數如下:

從 KleidiAI 內核的執行耗時來看,三款編譯器的表現十分接近,ATfE 略領先于 AC6(約 1%),而 GCC 則稍顯落后。在 -O2 和 -O3 級別下重新運行了該測試,如前文所述,隨著優化級別的提高,GCC 在 -O3 級別時小幅反超,這正是高級別優化帶來的提升效果之一。
在準備階段,ATfE 與 AC6 的表現依然接近,GCC 則仍然落后。同樣在 -O2 和 -O3 級別下重新測試后發現,在這些優化級別下,GCC 縮小了部分差距。這似乎表明,不同編譯器會在不同優化級別中納入特定的優化過程。
然而,ATfE 之所以能實現整體耗時的大幅縮短,關鍵提速點其實在啟動階段。我猜測,這可能是因為 ATfE 所使用的 Picolibc 在 C 庫設置環節,比 AC6 采用的 ArmCLib 或 GCC 采用的 newlib 更輕量化。由于 ATfE 的主要提速點在于此,而測試項目本身的代碼量較少,這就導致初始的性能對比結果存在偏差:如果增大工作負載,啟動代碼在整體運行時間中的占比就不會如此之高了。
分析編譯器優化
若要了解 ATfE 采用(或未采用)哪些優化過程,可借助編譯器選項-Rpass(或-Rpass-missed)。這兩個選項后可接=.*(表示所有優化過程)或=
快速查看了 ATfE 在 -O0、-O1、-O2 和 -O3 下級別下的優化過程,其結果如下:
即便在 -O0 級別,編譯器仍會對部分 Arm C 語言擴展 (ACLE) 內聯函數進行內聯處理,例如 vaddq_s16(向量加法)。這一點是合理的,因為這類調用僅對應單條指令,因此在性能(得益于消除函數調用開銷)與代碼體積增加(因代碼復制導致)之間不存在權衡問題。
在 -O1 級別,編譯器進行了大量的函數內聯,尤其是對小型函數(如隨機數生成器實現)。此外,若循環中某些指令或表達式無需在每次迭代時重新計算,編譯器會將它們提升 (hoist) 到循環外部。
在 -O2 級別,編譯器開始進行循環向量化,但部分向量化操作會推遲到 -O3 級別。編譯器采用啟發式算法來權衡每項優化的收益與成本。如同內聯優化,在同一優化級別下,不同循環可能會采用不同的向量化策略,這一點值得關注。
在 - O3 級別,編譯器還會對部分循環進行展開。
提升 (hoisting) 機制值得深入探究。以 KleidiAI 源文件中一段大幅簡化的代碼為例:
for (size_t dst_row_idx = 0; dst_row_idx < dst_num_rows; ++dst_row_idx) {?
for (size_t dst_byte_idx = 0; dst_byte_idx < dst_num_bytes_per_row; ++dst_byte_idx) {?
const size_t block_idx = dst_byte_idx / block_length_in_bytes;
const size_t nr_idx = block_idx % nr;
const size_t n0_idx = dst_row_idx * nr + nr_idx;
編譯器注意到,在計算 n0_idx 時,其中的乘法部分無需放在內層循環中,因為在內層循環中,dst_row_idx 和 nr 均為常量:
src/kai_rhs_pack_nxk_qsi4cxp_qs4cxs1s0.c47: remark: hoisting mul [-Rpass=licm]
96 | const size_t n0_idx = dst_row_idx * nr + nr_idx;
| ^
編譯器會將該乘法操作從內層循環提升 (hoist) 到外層循環,大致如下:
for (size_t dst_row_idx = 0; dst_row_idx < dst_num_rows; ++dst_row_idx) {?
const size_t hoist_temp = dst_row_idx * nr;
for (size_t dst_byte_idx = 0; dst_byte_idx < dst_num_bytes_per_row; ++dst_byte_idx) {?
const size_t block_idx = dst_byte_idx / block_length_in_bytes;
const size_t nr_idx = block_idx % nr;
const size_t n0_idx = hoist_temp + nr_idx;
開發者也可手動進行此類優化,但這可能會使代碼變得不夠簡潔、清晰,難以理解和維護。編譯器會考慮這些因素,從而讓開發者能夠專注于代碼功能、清晰度和可維護性。
ATfE 的 -Rpass 選項輸出包含大量信息,既涉及已應用的優化過程,也涉及未應用的過程。這些信息對于開發者而言非常有幫助,能讓開發者了解編譯器如何優化代碼,并指導開發者對代碼進行調整,以更好地配合編譯器優化。這是一個龐大的主題,我將在后續博客中深入探討。
結論
Arm Development Studio 提供了一套適用于裸機環境下 KleidiAI 內核實驗的工具,包括便于快速上手的示例項目、用于測試的 FVP,以及 AC6 的授權(之后還將包含 ATfEP 的授權)。與所有軟件開發工作一樣,在評估編譯器性能等指標時,需要考慮采集所有相關數據。在本案例中,很容易輕易得出“用 ATfE 構建的項目比用 AC6 構建的項目快約 20%”的結論。ATfE 會基于每項潛在優化的成本與收益做出啟發式優化決策,并提供實用選項來查看已采用和未采用的優化。通過這些選項獲取的信息,可用于調整代碼,使編譯器能夠實現更多優化。
-
ARM
+關注
關注
135文章
9552瀏覽量
391834 -
內核
+關注
關注
4文章
1467瀏覽量
42870 -
cpu
+關注
關注
68文章
11277瀏覽量
224942 -
人工智能
+關注
關注
1817文章
50094瀏覽量
265273
原文標題:在裸機 Arm 環境中運行 Arm KleidiAI MatMul 內核
文章出處:【微信號:Arm社區,微信公眾號:Arm社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
如何在嵌入式Linux開發板上配置Qt運行環境
如何在裸機系統中集成SystemView
如何在樹莓派上安裝并運行 Arduino 集成開發環境!
《電子發燒友電子設計周報》聚焦硬科技領域核心價值 第23期:2025.08.04--2025.08.08
【OK210試用體驗】之三裸機開發環境搭建
請問裸機程序怎么做才可以直接下載到SDRAM中運行?
可以將MCUXpresso用于該設備中M7內核的軟件開發,而不是A53內核,這是否正確?
如何使用J-Link在A55內核上進行i.MX93 EVK裸機調試?
環境監測設備中的FreeRTOS低功耗
如何在裸機環境中運行KleidiAI微內核
評論