C代碼調用匯編&使用指令集優化


  最近研究x264匯編代碼,感覺使用到的優化思想和手法非常不錯,在此寫一個demon來記錄我學習過程

  • 從搭建環境開始

  x264使用匯編優化的思想是將匯編代碼編譯到一個靜態庫里,供C代碼調用,所以首先需要構建一個匯編函數得靜態庫。因為手動配置使用yasm來編譯匯編文件,並生成一個lib相當麻煩,我選擇的是使用cmake來構建。

  在demon里有一個sum.asm的匯編文件,文件里是所有的匯編函數,通過yasm編譯后生成sum.obj,然后通過sum.obj來創建一個sum.lib庫供C代碼使用。還有一個main。c的C文件,用來生成可執行文件main,CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 3.0.00)

project (asm)

find_program(YASM_EXECUTABLE 
    NAMES yasm yasm-1.2.0-win32 yasm-1.2.0-win64
    HINTS $ENV{YASM_ROOT} ${YASM_ROOT}
    PATH_SUFFIXES bin
)
set(FLAGS -f win64 -DARCH_X86_64=1)
add_custom_command(
        OUTPUT sum.obj
        COMMAND ${YASM_EXECUTABLE}  ARGS ${FLAGS} ../source/sum.asm -o sum.obj
        DEPENDS sum.asm)

#添加靜態庫sum add_library( sum STATIC sum.obj sum.asm ) set_target_properties(sum PROPERTIES LINKER_LANGUAGE C)

#添加使用靜態庫的可執行程序main
add_executable( main main.c )
target_link_libraries(main sum )

  其中find_program是在系統環境變量中尋找看是否有yasm匯編器,在此假設是有的。

  需要注意的是在COMMAND ${YASM_EXECUTABLE} ARGS ${FLAGS} ../source/sum.asm -o sum.obj中指定匯編文件的路徑得是相對與工程文件所在的相對路徑,所以這里是../source/sum.asm

  至此環境搭建完畢,使用cmake就能生成需要的匯編lib工程和調用匯編函數得可執行文件工程。在vs上如下圖所示

  

  •  關於匯編

  先寫一個最簡單的例子(在此針對的是64bit匯編),假設main函數里需要對兩個數字求和,代碼如下:

1 int sum(int a, int b);//此函數通過匯編實現
2 
3 int main(int argc, char *argv[])
4 {
5     int num = sum(2, 3);
6     return 0;
7 }

  那麽對應的匯編實現sum函數的代碼如下:

1 global sum
2 
3 sum:
4 
5     add ecx, edx ;直接使用ecx和edx寄存器中的參數
6     mov eax, ecx
7 
8     ret

   這是一個最簡單的C調用匯編函數得demon,在寫匯編函數的時候碰到了以下問題:

  1. 之前學習的都是32位匯編的時候,函數參數的傳遞都是通過棧來完成的,在64位匯編中,前四個參數是通過寄存器rcx、rdx、r8、r9來傳遞的,只有參數個數大於4個后才通過棧來傳遞,所在在以上匯編代碼中直接使用了寄存器ecx和edx中的值

  當函數參數個數大於4時,假設C代碼如下:

1 int sum(int a, int b, int c, int d, int e);
2 
3 int main(int argc, char *argv[])
4 {
5     int num = sum(2, 3, 4, 5, 6);
6     return 0;
7 }

  對應的匯編代碼如下:

 1 global sum
 2 
 3 sum:
 4 
 5     add rcx, rdx
 6     add rcx, r8
 7     add rcx, r9
 8 
 9     mov rdx, [rsp + 40] ;從棧中取出第5個參數放入rdx寄存器
10     add rcx, rdx
11 
12     mov rax, rcx
13 
14     ret

  此處需要注意的是:前四個參數是通過寄存器傳遞,從棧中取出第五個參數時,並不是從rsp+8的地方取,而是從rsp+40(40 = 4*8 + 8)的地方取,說明雖然前四個參數是通過寄存器傳遞,但是在棧中還是占用了相應的空間,我對此的理解是為了__stdcall和__cdecll的兼容吧。

  • 使用指令集優化(SSE AVX等)

  首先來看一下SIMD寄存器

 

 

  SSE使用到的SIMD寄存器是128bit,一共有16個,從XMM0到XMM15

  AVX拓展出來的SIMD寄存器是256bit,一共也是16個,從YMM0到YMM16,當然AVX也能使用SSE的XMM寄存器

  AVX2.0的時候將寄存器拓展到了512bit,一共有32個,從ZMM0到ZMM31

  假設我們的main函數是對兩個數組進行求和,代碼如下:

 1 #define N 8
 2 
 3 int sum(float a[], float b[]);
 4 
 5 int sum_c(float a[], float b[])
 6 {
 7     for (int i = 0; i < N; i++)
 8     {
 9         a[i] += b[i];
10     }
11     return 0;
12 }
13 
14 int main(int argc, char *argv[])
15 {
16     float a[N] = { 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 };
17     float b[N] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 };
18     //將數組b[N]中的數據加到數組a[n]中
19     sum_c(a, b);//不使用匯編優化
20     sum(a, b);//使用匯編優化
21     return 0;
22 }

  可以看到,不使用匯編優化的話,在sum_c函數中,我們需要依次計算出a[i] + b[i]的和並保存在a[i]中。

  如果使用SSE指令集優化的話,代碼如下:

 1 global sum
 2 
 3 sum:
 4 
 5     movups xmm0, [rcx]
 6     movups xmm1, [rdx]
 7     movups xmm2, [rcx + 16]
 8     movups xmm3, [rdx + 16]
 9 
10     addps xmm0, xmm1
11     addps xmm2, xmm3
12 
13     movups [rcx], xmm0
14     movups [rcx + 16], xmm2
15     
16     ret

  可以看到,只需要進行兩次加法運算就能計算出a[8]和b[8]中8個數字相加的和,這里需要進行兩次計算是因為xmm寄存器是128bit,所以每次只能計算4個float數據,8個數據得分兩次計算。

  使用AVX指令集優化代碼如下:

 1 global sum
 2 
 3 sum:
 4 
 5     vmovups ymm1, [rcx]
 6     vmovups ymm2, [rdx]
 7 
 8     vaddps ymm0, ymm1, ymm2
 9     vmovups [rcx], ymm0
10     
11     ret

  因為AVX使用到了256bit的ymm寄存器,所以一次可以處理8個32bit的float數據,一次計算就能完成兩組8個float數據分別的求和操作。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM