2013年7月16日 星期二

直接交換兩個雙精度用 XMM 暫存器

好久沒發文了,在不發文就對不起博主給我這個寶貴的空間了,今天要探討的主題很簡單
也很好懂,設計一條可以交換兩個雙精度浮點數的函式,數值互換是很基本的數學函式
,但是卻很重要,不少演算法裡頭,都會用到這種基本功能,當然本文不會研究甚麼艱澀
的演算法,這個主題是要透過實作 交換兩個雙精度浮點數的函式 來讓讀者認識現在新的
x86 CPU 內部的 XMM 暫存器 與 初步的了解如何在組合語言中搬移雙精度浮點數。




浮點數其實有三種 單精度 雙精度 延伸精度 大部分程式語言能宣告的型式 只能使用
單精度 (single) 與 雙精度 (double),至於延伸精度 (extended) 就不太常見了,不過浮點
運算單元 (FPU) 的暫存器寬度倒是有 80-bit 寬,這 80-bit 寬就是延伸精度浮點數的寬度,但是
一般還是只會拿來存放 single floating-point 與 double floating-point。

各種浮點數的型態

Data Type        Length    Decimal digits Precision
Single               32          小數點 7   位
Double             64          小數點 15 位
Extended          80          小數點 19 位

交換兩個雙精度浮點數的函式 我們用 C 語言寫一個通用版的函式也很容易

void cSwapsd(double*p_s1, double*p_s2)
{
    double tmp = *p_s1;
    *p_s1 = *p_s2;
    *p_s2 = tmp;
}

這邊各位可以順便學習一下命名法則 c 代表此函式為 C 版本,Swap 是函式名稱,sd 代表參數
型態,sd 這個 suffix 是指 Scalar Double-Floating point,表示說這個函式的參數是 純量雙精度浮
點數,這種命名法是 OpenGL 程式庫函式命名慣例,以 glColor3f 為例

{ Root Command, Function Name, # of Args , Type } = glColor3f

gl 就是 Root Command,Color 就是 Function Name,3 就是 # of Args (參數個數), f 就是 Type 為
float,本文也採用這種命名法,不過我沒有使用 # of Args。

假如我們用 cSwapsd 這個版本來交換兩個雙精度浮點數,可以猜到,編譯器一定會使用
保守的 x87 浮點運算元來交換這兩個雙精度浮點數,用 fld fstp fxch 這三條 x87 浮點指令
就可以完成,怎麼說呢,只要用 fld 把 *p_s1 與 *p_s2 的值推入浮點暫存器堆疊空間,這時候
這兩個雙精度浮點就會存放在浮點暫存器堆疊空間的 ST(0) 與 ST(1),這時候使用 fxch 讓
ST(0) 與 ST(1) 的內容作交換,在使用 fstp 就可以完成 *p_s1 與 *p_s2 交換的動作,可是
x87 指令在執行上是屬於較為耗時的指令集,有沒有其他辦法來交換這兩個雙精度浮點數 ?
答案就是用另外一種暫存器跟操作它們的專屬指令集,也就是用 XMM 暫存器 與 SSE 指令集
近代的 x86 CPU 功能很強大,配備了新式的 多媒體延伸指令集 也就是 SSE,而這些指令集
的專屬暫存器就是 XMM 暫存器,白話講,就是又多了許多免費速度快的新型暫存器可以拿
來利用一番,其實 XMM 暫存器 與 SSE 指令集 出現很長的時間了,很可惜的是編譯器很難
加以利用,一般來說得要開發者自行利用,基本上你不用期待編譯器能夠把你的 C 程式利用
這些特殊的指令產生最佳化的組合語言,想要享受 CPU 新的特性,只能自己動手去呼叫這些
指令,那麼 XMM 是怎麼樣的一種暫存器呢 ? 我們看看這個圖


從 XMM 暫存器就知道,這種一種 Jumbo Register,意思就是巨型暫存器,光是容量就可以
塞進 4 個 單精度浮點,或是 2 個雙精度浮點,也就是說它是一個 128-bit Register,這種畫分
可是讓 SSE 指令集針對不同用途來使用 XMM Register,最常見的主要是當作四個 float 來使用
用於 ps (Parallel Scalar) 時,可以同時操作四個 float 作運算,用於 ss (Single Scalar) 時,拿 [31:0]
當作單一單精度浮點運算,另外,SSE 指令集的操作術語中,常常會看到 high packed 與
low packed,這是很重要的觀念,SSE 指令集還可以只對 XMM 暫存器操作半邊,也就是

high packed = {data3,data2}low packed = {data1,data0}

在這種向量指令的領域中,最後一個要認識的就是 unaligned 與 aligned 也就是資料的未對齊
對齊,所以 SSE 指令集中會看到有 u 與 a 這兩個字母,就是指這個意思,有對齊的資令存取
上效率高,因為是 CPU 存取資料的原生格式,怎麼知道資料有沒有對齊呢 ? 下面的公式
就可以判別 :

對齊的資料是指  ( 資料所在位址 ) mod ( 資料的大小 ) = 0 

能夠滿足這個條件的資料,這就是對齊的資料,也就是說光是 宣告 float v[4] 這樣沒有用,
你還得確認 v 的位址是不是在整除 16 的邊界上,這樣的資料與 XMM 暫存器 在交換資料
速度就會很快,對 XMM 暫存器重要的基本知識大致上都介紹了,我們趕快來看看今天的
問題怎麼用 SSE 指令實作,很明顯的我們只需要搬移 Single Double-Floating point 也就是說
我們必須使用 movsd 來對 記憶體 與 xmm 暫存器之間作資料移動,在 Intel 中的文獻對於
movsd 指令的介紹如下 :

Opcode

Instruction

Description

F2 0F 10 /r
MOVSD xmm1xmm2/m64
Move scalar double-precision floating-point value from xmm2/m64 to xmm1 register.
F2 0F 11 /r
MOVSD xmm2/m64xmm
Move scalar double-precision floating-point value from xmm1 register to xmm2/m64.

很簡單吧,可以 xmm 對 xmm 移動資料也可以對 m64 移動資料,這邊就要在次強調,一定
要學會使用獨立的組合語言程式設計,不要在繼續依賴於 C 內插組合語言了,因為這招在
64-bit 環境中就不支援了,還是老樣子,搬出 FASM 馬上寫個函式給 C 語言呼叫,下面就是
我實做好的 FASM Code :

; function prototype: void fasmSwapsd(double *p_s1,double *p_s2)

include 'win32a.inc'

format MS COFF

public fasmSwapsd as '_fasmSwapsd'

;code section
section '.text' code readable executable

fasmSwapsd:
    ;create stack frame (prologue code)
    ;the return address is now located at ebp+4
    push ebp
    mov  ebp,esp

    label .p_s1 dword at ebp+8  ;p_s1 arg is now located at ebp+8
    label .p_s2 dword at ebp+12 ;p_s2 arg is now located at ebp+12

    ;registers ebx esi edi need to be preserved
    push ebx esi edi
    ;========= end of prologue code =========

    mov   eax ,dword [.p_s1]
    movsd xmm0,qword [eax]   ; *p_s1 to xmm0
    mov   ecx ,dword [.p_s2]
    movsd xmm1,qword [ecx]   ; *p_s2 to xmm1

    movsd qword [ecx],xmm0 ; xmm0 to *p_s2
    movsd qword [eax],xmm1 ; xmm1 to *p_s1

    ;========= epilogue code =========
    pop edi esi ebx ;restore registers
    mov esp, ebp    ;set esp back to stack frame
    pop ebp         ;destroy stack frame
    retn 2*0        ;return and restore arguments from stack 

其中 prologue 與 epilogue code 以前呼叫慣例的文章就討論很多一次,而 public format label
include section 等關於 FASM 本身語法的部分實戰呼叫慣例也解說過了,因此我們就把焦點
放在中間的部分 :

可以看到 所謂的 m64 型態的記憶體,寫 FASM 的時候 要使用 qword 指向所要解讀的位址內
容,讓 movsd 指令將位於 qword 指向的位址內容的 double 型浮點數移動到 xmm reg,因此
我們一旦使用的 SSE 指令要交換兩個浮點數就變得很容易了,原因是 movsd 本身直接就可以
移動這種 m64 的資料,那我們當然只要暫存於兩個 xmm reg 上然後在交叉回存這樣資料就可
以交換了,movsd 指令讓我們擁有類似像 mov 搬移 32-bit 整數那樣的方便,只是操作的對象
要換成 xmm 暫存器而已,相信聰明的你一看原始碼就懂,另外要注意這次我寫的這個給 C
語言呼叫的 fasmSwapsd 是 __cdecl 的呼叫慣例,在 C 語言內呼叫的時候並不需要加 __stdcall
去改變函式的呼叫慣例,用預設的 C 呼叫慣例即可。

注 :
假如你是用 MASM 的話 那個 qword 就要改成用 mmword 這邊語法會不太一樣,而且要外加
ptr 關鍵字,也就是說 MASM 語法要寫為 movsd mmword ptr [eax],xmm1 。

接下來就是 C 程式呼叫的部分,這邊你會學到一個新的 MS 的延伸關鍵字
                                                    
                                                __declspec(align(X))

此關鍵字放在宣告的變數前,就可以控制編譯後的變數會被放置在整除 X 位址的邊界上,
所以測試的 C 程式也很簡單,如下:

extern void fasmSwapsd(double *p_s1,double *p_s2);

int main(void)
{
    __declspec(align(16)) double f64A = 3.14;
    __declspec(align(16)) double f64B = 6.28;
    
    printf("f64A = %.4lf, f64B = %.4lf\n", f64A, f64B);
    
    fasmSwapsd(&f64A, &f64B);
    
    printf("f64A = %.4lf, f64B = %.4lf\n", f64A, f64B);
    
    return 0;
}

下面是執行結果:


我直接擷取整個 Console 畫面,讓讀者可以順便複習怎麼用指令簡單編譯的方法。

您是否也做出了一樣的結果呢 ?

沒有留言:

張貼留言