一.Xilinx Zynq-7000帶來新的設計思路
在以前,我們的單板上往往有CPU和多片FPGA,由CPU完成系統的配置和管理,FPGA完成特定算法的硬件加速,受限于CPU和FPGA之間的通信帶寬和延遲,CPU和FPGA之間的接口大多是用于配置和管理,無法傳輸大量的數據。
Xilinx推出的Zynq-7000系列芯片很好的解決了這一問題。它內含硬化好的CPU核和常見的外設(DRAM控制器,千兆以太網,USB 2.0 OTG,SD card控制器,FLASH控制器,UART,CAN,SPI,I2C等等 ),這一部分被稱為Processing System(簡稱PS),它可以完全獨立于FPGA運行;Zynq-7000芯片內部還有容量不等的FPGA資源,被稱為Programmable Logic(簡稱PL),可以支持不同復雜度的邏輯設計。最重要的是,在PS和PL之間,有超過3000根的互聯信號,包括9路AXI通道,可以提供大約100Gb/s的通信帶寬,同時在PS和PL之間還有DMA,Interrupt和EMIO等多種資源。這就使得數據可以在PS和PL之間靈活高效的遷移,從系統設計的角度上來講,任務可以在軟件和硬件之間靈活的分割,實現高度優化的系統設計。這也給嵌入式系統的開發方法提供了新的思路和流程:首先利用軟件可以快速靈活編程的特點,快速的用軟件實現系統的原型;然后通過對軟件進行profiling找出對系統性能影響最大的代碼,將這部分代碼用FPGA來硬件加速,實現高度優化的嵌入式系統;Xilinx還提供了HLS(High Level Synthesis)工具可以方便快速的把軟件代碼轉化成RTL代碼,幫助開發者快速的實現基于FPGA的硬件加速器。
在這一流程中,重要的一環是如何找出軟件中對性能影響最大的那部分代碼。對于簡單的應用,我們可以很容易的判斷出來,例如對頻譜分析來說,FFT算法就是最至關重要的需要優化的算法。但是在很多時候,軟件非常復雜,有很多的復雜的函數調用,很難通過靜態的觀察和分析找出對性能影響最大的那部分代碼,這時就需要通過profiling工具,在軟件動態運行中收集數據,通過統計的方法找出核心代碼了。
二.Profiling的對象
在Linux下有很多profiling工具,各自有自己的優勢和劣勢。在這里我們重點研究一下如何使用gprof對軟件做profiling。
很多介紹profiling工具的文章都是開發者自己寫一個簡單源文件,里面有簡單的函數調用。
為了更好的展示profiling的效果,這里我們沒有采用這種方法,而是采用了一個相對比較復雜的軟件包libjpeg。
libjpeg 是一個完全用C語言編寫的庫,包含了被廣泛使用的JPEG解碼、JPEG編碼和其他的JPEG功能的實現。這個庫由獨立JPEG工作組維護。編譯完成后除了相應的.a和.so庫文件之外,還會生成以下工具程序:
cjpeg和djpeg:用于JPEG的壓縮和解壓縮,可以和一些其他格式的圖形文件進行轉換。
rdjpgcom和wrjpgcom:用于在JFIF文件中插入和提取文字信息。
jpegtran:一個用于在不同的JPEG格式之間進行無損轉換的工具。
在這里cjpeg和djpeg就是很不錯的profiling對象,有一定的復雜度,但又沒有復雜到令人生畏。JPEG圖像文件可以在互聯網上靈活選取,基本原則是足夠大,這樣可以有比較長的運行時間來收集profiling數據,同時有足夠的細節可以讓軟件充分的運行起來。網站 上有很多大的圖片,筆者選擇的是一個2880x1800的JPEG文件。
Libjpeg可以在 上找到。這里使用的版本是13-Jan-2013發布的release 9。下載后的源文件是jpegsrc.v9.tar.gz
三. GNU profiler(gprof)簡介
GNU profiler(gprof)是GNU Binutils( https://sourceware.org/binutils/ )的一個組成部分,詳細的文檔可以在 https://sourceware.org/binutils/docs/gprof/ 找到,默認情況下Linux系統當中都帶有這個工具,不過如果打算在嵌入式開發板上用還是需要對GNU Binutils做交叉編譯的。
Gprof的功能:
1. 生成“flat profile”,包括每個函數的調用次數,每個函數消耗的處理器時間,
2. 生成“Call graph”,包括函數的調用關系,每個函數調用花費了多少時間。
3. 生成“注釋的源代碼”,即是程序源代碼的一個復本,標記有程序中每行代碼的執行次數。
Gprof的原理:
通過在編譯和鏈接時使用 -pg選項,gcc 在應用程序的每個函數中都加入了一個名為mcount (也可能是”_mcount”或者”__mcount”, 依賴于編譯器或操作系統)的函數,這樣應用程序里的每一個函數都會調用mcount, 而mcount 會在內存中保存一張函數調用圖,記錄通過函數調用堆棧找到的子函數和父函數的地址,以及所有與函數相關的調用時間,調用次數等信息。
Gprof基本使用流程
1. 在編譯和鏈接時加上-pg選項。一般可以加在 Makefile 中的CFLAGS和LDFLAGS中。
2. 執行編譯的二進制程序。執行參數和方式同以前。
3. 正常結束進程。這時內存中的信息會被寫入到程序運行目錄下的gmon.out 文件中。
4. 用 gprof 工具分析 gmon.out 文件。
Gprof參數說明
? -b 不再輸出統計圖表中每個字段的詳細描述。
? -p 只輸出函數的調用圖(Call graph的那部分信息)。
? -q 只輸出函數的時間消耗列表。
? -e Name 不輸出函數Name 及其子函數的調用圖(除非它們有未被限制的其它父函數)。可以給定多個-e 標志。一個 -e 標志只能指定一個函數。
? -E Name 不輸出函數Name 及其子函數的調用圖,此標志類似于 -e 標志,但它在總時間和百分比時間的計算中排除了由函數Name 及其子函數所用的時間。
? -f Name 輸出函數Name 及其子函數的調用圖。可以指定多個 -f 標志。一個 -f 標志只能指定一個函數。
? -F Name 輸出函數Name 及其子函數的調用圖,它類似于 -f 標志,但它在總時間和百分比時間計算中僅使用所打印的例程的時間。可以指定多個 -F 標志。一個 -F 標志只能指定一個函數。-F 標志覆蓋 -E 標志。
一般用法:
gprof -b ELF_file_name gmon.out >report.txt
Gprof報告中flat profile表格各列的說明:
%time: 該函數消耗時間占程序所有時間百分比,全部相加應該是100%。
Cumulative seconds: 程序的累積執行時間,包括表格內該函數所在行之上的所有函數的執行時間
Self Seconds: 該函數本身的全部執行時間。表格會依照這列的數值按照降序排序所有行
Calls: 函數被調用次數, 如果無法確定則為空。
Self ms/call: 函數平均執行時間。
Total ms/call: 函數平均執行時間, 包括其內部調用。
Name: 函數名。在按照self seconds和calls排序后再依照這列進行字母排序。
Gprof報告中Call Graph表格各列的說明:
Index: 索引值
%time: 函數消耗時間占所有時間百分比
Self: 函數本身執行時間
Children: 執行子函數所用時間
Called: 被調用次數
Name: 函數名
Gprof的優勢:
1. 簡單易用。只需要在編譯和鏈接是增加-pg選項。gprof對于代碼大部分是用戶空間的CPU密集型的應用程序用處明顯,對于大部分時間運行在內核空間或者由于外部因素(例如操作系統的 I/O 子系統過載)而運行緩慢的應用程序則意義不大。
2. GNU Binutils的組成部分,基本上任何Linux里面都有。可以把生成的gmon.out拷貝到host上進行分析,省掉了一部分交叉編譯的工作量。
Gprof的劣勢:
1. Gprof只夠監控到編譯和鏈接時有-pg選項的函數,工作在內核態的函數和沒有加-pg編譯的第三方庫函數是無法被gprof監控到的。因此Gprof比較適合執行時間大部分在用戶態的應用。在使用Gprof前最好用Linux下的time命令來確認應用程序的實際運行時間、用戶空間運行時間、內核空間運行時間,以判斷是否合適用gprof。Oprofile可以解決這一問題。
2. Gprof不能監控shared library,即.so的文件。
對此有詳細的分析。對這類文件可以用sprof,不過并不好用。變通的辦法是將library靜態鏈接到應用中,這樣會增加應用程序的code size。
3. Gprof 不支持多線程應用,多線程下只能采集主線程性能數據。原因是在多線程內只有主線程才能響應gprof采用的ITIMER_PROF信號。有一個簡單的方法可以解決這一問題:
4. gprof只能在程序正常結束退出,或者通過系統調用exit()退出之后才能生成報告(gmon.out)。原因是gprof通過在atexit()里注冊了一個函數來產生結果信息,任何非正常退出都不會執行atexit()的動作,所以不會產生gmon.out文件。
5. 函數執行時間是估計值。函數執行時間是通過采樣估算的, 在執行時間足夠長的情況下,這個不是什么大的問題,一般估算值與實際值相差不大。
四.Gprof在Zynq-7000開發板上的實驗:
Hardware: ZC706 evaluation board(其他開發板亦可,只是細節上會略有不同)
Software: Xilinx 14.7 Linux pre-built
Tool chain: PetaLinux 2013.04 tool chain
為了簡單起見,筆者沒有重新編譯Linux,而是使用的Xilinx 14.7 Linux pre-built。
在Linux Host上下載libjpeg后執行以下命令即可完成編譯:
cd
tar zxvf /path/to/jpegsrc.v9.tar.gz
cd jpeg-9
./configure --prefix=/home/wave/xilinx/libjpeg/jpeg-bin --host=arm-xilinx-linux-gnueabi
Note: 參數--prefix指明編譯結果的安裝位置,參數--host指明交叉編譯工具鏈的前綴。在使用PetaLinux 2013.04 tool chain之前需要先到其目錄下source settings.sh
這里需要編輯Makefile,在CFLAGS和LDFLAGS中增加-pg選項。
make
make install
這時編譯完成后的可執行程序cjpeg和djpeg使用到了.so文件,不適合用gprof。關于這一點可以用ldd命令確認。所以還需要用以下命令手工編譯出statically linked binary,拷貝到libjpeg安裝目錄下的bin目錄下,方便后期的profiling。這些命令可以通過觀察libjpeg的make過程得到。
arm-xilinx-linux-gnueabi-gcc -std=gnu99 -g -O2 -pg -o djpeg-s djpeg.c wrppm.c wrgif.c wrtarga.c wrrle.c wrbmp.c rdcolmap.c cdjpeg.c ../jpeg-bin/lib/libjpeg.a
arm-xilinx-linux-gnueabi-gcc -std=gnu99 -g -O2 -pg -o cjpeg-s cjpeg.c rdppm.c rdgif.c rdtarga.c rdrle.c rdbmp.c rdswitch.c cdjpeg.c ../jpeg-bin/lib/libjpeg.a
然后將jpeg-bin下的所有內容打包,和Xilinx 14.7 Linux pre-built image files,以及數據文件park-2880x1800.jpg拷貝到SD卡中,從SD卡啟動ZC706開發板。
在開發板的console上,執行以下命令
mount /dev/mmcblk0p1 /mnt
mkdir work
cd work
tar zxvf /mnt/jpeg-bin.tar.gz
cd jpeg-bin/bin
cp /mnt/park-2880x1800.jpg .
export LD_LIBRARY_PATH=/home/root/work/jpeg-bin/lib
time ./djpeg-s -bmp park-2880x1800.jpg > result.bmp
mv gmon.out gmon-ds.out
time ./cjpeg-s ./result.bmp > ./result.jpg
mv gmon.out gmon-cs.out
對于djpeg-s和cjpeg-s,執行時間如下所示。我們可以看到這兩個應用程序的主要執行時間實在用戶空間的,還是比較適合用gprof來做profiling的。
real 0m4.258s
user 0m4.200s
sys 0m0.050s
real 0m4.289s
user 0m4.230s
sys 0m0.050s
然后我們可以把執行結果拷貝到SD卡中,準備拿到Linux Host上進行分析。
cp result.* /mnt
cp gmon*.out /mnt
umount /mnt
在Linux Host上,我們可以通過以下命令看到profiling的結果:
gprof -b djpeg-s gmon-ds.out >report-ds.txt
gprof -b cjpeg-s gmon-cs.out >report-cs.txt
關于JPEG解碼Profiling結果的主要部分如下:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
20.95 0.31 0.31 1800 0.17 0.17 ycc_rgb_convert
19.60 0.60 0.29 40680 0.01 0.01 jpeg_idct_16x16
17.57 0.86 0.26 20340 0.01 0.02 decode_mcu
14.87 1.08 0.22 81000 0.00 0.00 jpeg_idct_islow
13.51 1.28 0.20 finish_output_bmp
6.08 1.37 0.09 1175224 0.00 0.00 jpeg_fill_bit_buffer
5.41 1.45 0.08 put_pixel_rows
2.03 1.48 0.03 127506 0.00 0.00 jpeg_huff_decode
關于JPEG編碼Profiling結果的主要部分如下:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
24.63 0.50 0.50 preload_image
21.18 0.93 0.43 20340 0.02 0.02 encode_mcu_huff
13.79 1.21 0.28 40680 0.01 0.01 jpeg_fdct_16x16
12.81 1.47 0.26 81180 0.00 0.01 forward_DCT
8.87 1.65 0.18 81000 0.00 0.00 jpeg_fdct_islow
8.37 1.82 0.17 1800 0.09 0.09 rgb_ycc_convert
5.42 1.93 0.11 1 110.00 110.00 get_24bit_row
3.45 2.00 0.07 __divsi3
0.49 2.01 0.01 113 0.09 10.27 compress_data
0.49 2.02 0.01 __aeabi_uidivmod
0.49 2.03 0.01 jpeg_fdct_ifast
怎么樣?是不是很容易?
如果反復profiling幾次,就會注意到profiling結果里面的順序會有所變化。主要原因還是采樣的時間太短,只有4.2秒,如果延長profiling的時間,得到的結果會更加逼近真實值。
五.關于sprof:
sprof主要用于Gprof的補充,分析程序的共享庫(需要-g編譯)。一般的使用步驟:
1. export LD_PROFILE_OUTPUT=${PWD}
2. export LD_PROFILE=abc.so.A.B
3. export LD_LIBRARY_PATH=/path/to/lib/
4. 執行使用該so的主程序
5. 執行sprof so_file_name.so so_file_name.so.profile
注意:在實際執行時發現LD_PROFILE指向的文件名后有可能需要加上實際的數字才可以。
在本次實驗中,在生成profiling report的時候會發生錯誤:
sprof libjpeg.so.9 libjpeg.so.9.profile
Inconsistency detected by ld.so: dl-open.c: 611: _dl_open: Assertion `_dl_debug_initialize (0, args.nsid)->r_state == RT_CONSISTENT' failed!
按照Google Search Result的說法,在老版本的glibc里面會有這個問題,新版本有可能已經解決了。不過因為oprofile完全可以profiling shared library,所以只是簡單的嘗試了一下,沒有繼續深入研究這個話題。上面的經驗或許會對有興趣的開發者有所借鑒。
六.小結:
盡管gprof有各種這樣那樣的限制和不足,如果能夠合理規避,對于代碼執行時間大部分是在用戶空間的計算密集型的應用程序,gprof還是非常方便好用的。
評論
查看更多