官术网_书友最值得收藏!

2.2 浮點數類型

計算機也需要運算和存儲數學中的實數。在計算機的發展過程中,曾產生過多種存儲實數的方式,有的現在已經很少使用了。不管如何存儲,都可以將其劃分為定點實數存儲方式和浮點實數存儲方式兩種。所謂定點實數,就是約定整數位和小數位的長度,比如用4字節存儲實數,我們可以約定兩個高字節存放整數部分,兩個低字節存儲小數部分。

這樣做的好處是計算效率高,缺點也顯而易見:存儲不靈活,比如我們想存儲65536.5,由于整數的表達范圍超過了2字節,就無法用定點實數存儲方式了。對應地,也有浮點實數存儲方式,道理很簡單,就是用一部分二進制位存放小數點的位置信息,我們可以稱之為“指數域”,其他的數據位用來存儲沒有小數點時的數據和符號,我們可以稱之為“數據域”“符號域”。在訪問時取得指數域,與數據域運算后得到真值,如67.625,利用浮點實數存儲方式,數據域可以記錄為67625,小數點的位置可以記為10的-3次方,對該數進行訪問時計算一下即可。

浮點實數存儲方式的優缺點和定點實數存儲方式的正好相反。在80286之前,程序員常常為實數的計算傷腦筋,而后來出現的浮點協處理器,可以協助主處理器分擔浮點運算,程序員計算實數的效率因此得到提升,于是浮點實數存儲方式也就普及開來,成為現在主流的實數存儲方式。但是,在一些條件惡劣的嵌入式開發場合,仍可看到定點實數的存儲和使用。

在C/C++中,使用浮點方式存儲實數,用兩種數據類型來保存浮點數:float(單精度)、double(雙精度)。float在內存中占4字節,double在內存中占8字節。由于占用空間大,double可描述的精度更高。這兩種數據類型在內存中同樣以十六進制方式存儲,但與整型類型有所不同。

整型類型是將十進制轉換成二進制保存在內存中,以十六進制方式顯示。浮點類型并不是將一個浮點小數直接轉換成二進制數保存,而是將浮點小數轉換成的二進制碼重新編碼,再進行存儲。C/C++的浮點數是有符號的。

在C/C++中,將浮點數強制轉換為整數時,不會采用數學上四舍五入的方式,而是舍棄掉小數部分(第4章會提到的“向零取整”),不會進位。

浮點數的操作不會用到通用寄存器,而是會使用浮點協處理器的浮點寄存器,專門對浮點數進行運算處理。

2.2.1 浮點數的編碼方式

浮點數編碼轉換采用的是IEEE規定的編碼標準,float和double這兩種類型數據的轉換原理相同,但由于表示的范圍不一樣,編碼方式有些許區別。IEEE規定的浮點數編碼會將一個浮點數轉換為二進制數。以科學記數法劃分,將浮點數拆分為3部分:符號、指數、尾數。

1. float類型的IEEE編碼

float類型在內存中占4字節(32位)。最高位用于表示符號,在剩余的31位中,從左向右取8位表示指數,其余表示尾數,如圖2-2所示。

圖2-2 float類型的二進制表示說明

在進行二進制轉換前,需要對單精度浮點數進行科學記數法轉換。例如,將float類型的12.25f轉換為IEEE編碼,須將12.25f轉換成對應的二進制數1100.01,整數部分為1100,小數部分為01;小數點向左移動,每移動1次,指數加1,移動到除符號位的最高位為1處,停止移動,這里移動3次。對12.25f進行科學記數法轉換后二進制部分為1.10001,指數部分為3。在IEEE編碼中,由于在二進制情況下,最高位始終為1,為一個恒定值,故將其忽略不計。這里是一個正數,所以符號位添加0。

12.25f經IEEE轉換后各位如下。

  • 符號位:0。
  • 指數位:十進制3+127=130,轉換為二進制為10000010。
  • 尾數位:10001 000000000000000000(當不足23位時,低位補0填充)。

由于尾數位中最高位1是固定值,故忽略不計,只要在轉換回十進制數時加1即可。為什么指數位要加127呢?這是因為指數可能出現負數,十進制數127可表示為二進制數01111111,IEEE編碼方式規定,當指數小于0111111時為一個負數,反之為正數,因此01111111為0。

將示例中轉換后的符號位、指數位和尾數位按二進制拼接在一起,就成為一個完整的IEEE浮點編碼:01000001010001000000000000000000。轉換成十六進制數為0x41440000,內存中以小尾方式進行排列,故為00 00 44 41,分析結果如圖2-3所示。

圖2-3 單精度浮點數12.25f轉換為IEEE編碼

上面演示了符號位為正、指數位也為正的情況。那么什么情況下指數位可以為負呢?根據科學記數法,小數點向整數部分移動時,指數做加法。相反,小數點向小數部分移動時,指數需要以0起始做減法。浮點數-0.125f轉換IEEE編碼后,將會是一個符號位為1、指數部分為負的小數。-0.125f經轉換后二進制部分為0.001,用科學記數法表示為1.0,指數為-3。

-0.125fIEEE轉換后各位的情況如下。

  • 符號位:1。
  • 指數位:十進制127+(-3),轉換為二進制是01111100,如果不足8位,則高位補0。
  • 尾數位:00000000000000000000000。

-0.125f轉換后的IEEE編碼二進制拼接為10111110000000000000000000000000。轉換成十六進制數為0xBE000000,內存中顯示為00 00 00 BE,分析結果如圖2-4所示。

圖2-4 單精度浮點數-0.125f轉換為IEEE編碼

上面的兩個浮點數小數部分轉換為二進制時都是有窮的,如果小數部分轉換為二進制時得到一個無窮值,則會根據尾數部分的長度舍棄多余的部分。如單精度浮點數1.3f,小數部分轉換為二進制就會產生無窮值,依次轉換為0.3、0.6、1.2、0.4、0.8、1.6、1.2、0.4、0.8……轉換后得到的二進制數為1.01001100110011001100110,到第23位終止,尾數部分無法保存更大的值。

1.3f經IEEE轉換后各位的情況如下。

  • 符號位:0。
  • 指數位:十進制0+127,轉換二進制01111111。
  • 尾數位:01001100110011001100110。

1.3f轉換后的IEEE編碼二進制拼接為00111111101001100110011001100110。轉換成十六進制數為0x3FA66666,在內存中顯示為66 66 A6 3F。由于在轉換二進制過程中產生了無窮值,舍棄了部分位數,所以進行IEEE編碼轉換后得到的是一個近似值,存在一定的誤差。再次將這個IEEE編碼值轉換成十進制小數,得到的值為1.2516582,四舍五入保留一位小數之后為1.3。這就解釋了為什么C++在比較浮點數值是否為0時,要做一個區間比較而不是直接進行等值比較。正確浮點數比較的代碼見代碼清單2-1。

代碼清單2-1 正確浮點數比較

float f1 = 0.0001f; // 精確范圍
if (f2 >= -f1 && f2 <= f1)
{
    // f1等于0
}

2. double類型的IEEE編碼

前文講解了單精度浮點類型的IEEE編碼。double類型和float類型大同小異,只是double類型表示的范圍更大,占用空間更多,是float類型所占空間的兩倍。當然,精準度也會更高。

double類型占8字節的內存空間,同樣,最高位也用于表示符號,指數位占11位,剩余52位表示位數。

在float類型中,指數位范圍用8位表示,加127后用于判斷指數符號。在double類型中,由于擴大了精度,因此指數范圍使用11位正數表示,加1023后可用于指數符號判斷。

double類型的IEEE編碼轉換過程與float類型一樣,讀者可根據float類型的轉換流程來轉換double類型,此處不再贅述。

2.2.2 基本的浮點數指令

前面介紹了浮點數的編碼方式,下面我們來學習浮點數指令。浮點數的操作指令與普通數據類型不同,浮點數操作是通過浮點寄存器實現的,而普通數據類型使用的是通用寄存器,它們分別使用兩套不同的指令。

在早期CPU中,浮點寄存器是通過棧結構實現的,由ST(0)~ST(7)共8個棧空間組成,每個浮點寄存器占8字節。每次使用浮點寄存器都是率先使用ST(0),而不能越過ST(0)直接使用ST(1)。浮點寄存器的使用就是壓棧、出棧的過程。當ST(0)中存在數據時,執行壓棧操作后,ST(0)中的數據將裝入ST(1)中,如無出棧操作,將按順序向下壓棧,直到將浮點寄存器占滿為止。常用浮點數指令的介紹如表2-1所示,其中,IN表示操作數入棧,OUT表示操作數出棧。

表2-1 常用浮點數指令表

其他運算指令和普通指令類似,只須在前面加F即可,如FSUB和FSUBP等。

在使用浮點指令時,都要先利用ST(0)進行運算。當ST(0)中有值時,便會將ST(0)中的數據順序向下存放到ST(1)中,然后再將數據放入ST(0)。如果再次操作ST(0),則會先將ST(1)中的數據放入ST(2),然后將ST(0)中的數據放入ST(1),最后將新的數據存放到ST(0)。以此類推,在8個浮點寄存器都有值的情況下繼續向ST(0)中的存放數據,這時會丟棄ST(7)中的數據信息。

1997年開始,Intel和AMD都引入了媒體指令(MMX),這些指令允許多個操作并行,允許對多個不同的數據并行執行同一操作。近年來,這些擴展有了長足的發展。名字經過了一系列的修改,從MMX到SSE(流SIMD擴展),以及最新的AVX(高級向量擴展)。每一代都有一些不同的版本。每個擴展都用來管理寄存器中的數據,這些寄存器在MMX中被稱為MM寄存器,在SSE中被稱為XMM寄存器,在AVX中被稱為YMM寄存器。MM寄存器是64位的,XMM是128位的,而YMM是256位的。每個YMM寄存器可以存放8個32位值或4個64位值,可以是整數,也可以是浮點數。YMM寄存器一共有16個(YMM0~YMM15),而XMM是YMM的低128位。常用SSE浮點數指令的介紹如表2-2所示。

表2-2 常用SSE浮點數指令表

下面通過一個簡單的示例介紹各指令的使用流程,幫助讀者熟悉浮點指令的使用方法,如代碼清單2-2所示。

代碼清單2-2 Debug版float指令練習

// C+源碼
#include <stdio.h>
int main(int argc, char* argv[]) {
  float f = (float)argc;
  printf("%f", f);
  argc = (int)f;
  printf("%d", argc);
  return 0;
}
//x86_vs對應匯編代碼講解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  push    ecx
00401004  cvtsi2ss xmm0, dword ptr [ebp+8]
00401009  movss   [ebp-4], xmm0                         ;f = (float)argc;
0040100E  cvtss2sd xmm0, dword ptr [ebp-4];xmm0=(double)f
00401013  sub     esp, 8
00401016  movsd   qword ptr [esp], xmm0                 ;參數2 xmm0入棧
0040101B  push    offset asc_412160                     ;參數1 "%f"入棧
00401020  call    sub_401090                            ;調用printf函數
00401025  add     esp, 0Ch                              ;平衡棧
00401028  cvttss2si eax, dword ptr [ebp-4]
0040102D  mov     [ebp+8], eax ;argc=(int)f
00401030  mov     ecx, [ebp+8]
00401033  push    ecx                                   ;參數2 argc入棧
00401034  push    offset aD                             ;參數1 "%d"入棧
00401039  call    sub_401090                            ;調用printf函數
0040103E  add     esp, 8                                ;平衡棧
00401041  xor     eax, eax
00401043  mov     esp, ebp
00401045  pop     ebp
00401046  retn

//x86_gcc對應匯編代碼講解
00401510  push    ebp
00401511  mov     ebp, esp
00401513  and     esp, 0FFFFFFF0h                       ;棧對齊
00401516  sub     esp, 30h
00401519  call    ___main                               ;調用初始化函數
0040151E  fild    [ebp+8]                               ;argc轉換雙精度數入棧
00401521  fstp    dword ptr [esp+2Ch]                   ;f=(float)argc
00401525  fld     dword ptr [esp+2Ch]
00401529  fstp    qword ptr [esp+4]                     ;參數2 (double)f入棧
0040152D  mov     dword ptr [esp], offset asc_404000    ;參數1 "%f"入棧
00401534  call    _printf                               ;調用printf函數
00401539  fld     dword ptr [esp+2Ch]                   ;f入棧st(0)
0040153D  fnstcw  word ptr [esp+1Eh]
00401541  movzx   eax, word ptr [esp+1Eh]
00401546  or      ah, 0Ch
00401549  mov     [esp+1Ch], ax
0040154E  fldcw   word ptr [esp+1Ch]                    ;浮點異常檢查代碼
00401552  fistp   [ebp+8] ;argc=(int)f
00401555  fldcw   word ptr [esp+1Eh]
00401559  mov     eax, [ebp+8]
0040155C  mov     [esp+4], eax                          ;參數2 argc入棧
00401560  mov     dword ptr [esp], offset aD            ;參數1 "%d"入棧
00401567  call    _printf                               ;調用printf函數
0040156C  mov     eax, 0
00401571  leave
00401572  retn

//x86_clang對應匯編代碼講解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  sub     esp, 24h
00401006  mov     eax, [ebp+0Ch]                        ;eax=argv
00401009  mov     ecx, [ebp+8]                          ;ecx=argc
0040100C  mov     dword ptr [ebp-4], 0
00401013  mov     edx, [ebp+8]                          ;edx=argc
00401016  cvtsi2ss xmm0, edx                            ;xmm0=(int)argc
0040101A  movss   dword ptr [ebp-8], xmm0               ;f=(int)argc
0040101F  movss   xmm0, dword ptr [ebp-8]
00401024  cvtss2sd xmm0, xmm0                           ;xmm0=(double)f
00401028  lea     edx, asc_412160                       ;edx="%f"
0040102E  mov     [esp], edx                            ;參數1 "%f"入棧
00401031  movsd   qword ptr [esp+4], xmm0               ;參數2 xmm0入棧
00401037  mov     [ebp-0Ch], eax
0040103A  mov     [ebp-10h], ecx
0040103D  call    sub_401070                            ;調用printf函數
00401042  cvttss2si ecx, dword ptr [ebp-8]
00401047  mov     [ebp+8], ecx                          ;argc=(int)f
0040104A  mov     ecx, [ebp+8]
0040104D  lea     edx, aD ;edx="%d"
00401053  mov     [esp], edx                            ;參數1 "%d"入棧
00401056  mov     [esp+4], ecx                          ;參數2 argc入棧
0040105A  mov     [ebp-14h], eax
0040105D  call    sub_401070                            ;調用printf函數
00401062  xor     ecx, ecx
00401064  mov     [ebp-18h], eax
00401067  mov     eax, ecx
00401069  add     esp, 24h
0040106C  pop     ebp
0040106D  retn
//x64_vs對應匯編代碼講解
0000000140001000  mov     [rsp+10h], rdx
0000000140001005  mov     [rsp+8], ecx
0000000140001009  sub     rsp, 38h
000000014000100D  cvtsi2ss xmm0, dword ptr [rsp+40h]    ;xmm0=(float)argc
0000000140001013  movss   dword ptr [rsp+20h], xmm0     ;f=(float)argc
0000000140001019  cvtss2sd xmm0, dword ptr [rsp+20h]    ;xmm0=(double)f
000000014000101F  movaps  xmm1, xmm0
0000000140001022  movq    rdx, xmm1                     ;參數2 (double)f
0000000140001027  lea     rcx, asc_1400122C0            ;參數1 "%f"
000000014000102E  call    sub_1400010C0                 ;調用printf函數
0000000140001033  cvttss2si eax, dword ptr [rsp+20h]    ;eax=(int)f
0000000140001039  mov     [rsp+40h], eax                ;argc=(int)f
000000014000103D  mov     edx, [rsp+40h]                ;參數2 argc
0000000140001041  lea     rcx, aD                       ;參數1 "%d"
0000000140001048  call    sub_1400010C0                 ;調用printf函數
000000014000104D  xor     eax, eax
000000014000104F  add     rsp, 38h
0000000140001053  retn;
//x64_gcc對應匯編代碼講解
0000000000401550  push    rbp
0000000000401551  mov     rbp, rsp
0000000000401554  sub     rsp, 30h
0000000000401558  mov     [rbp+10h], ecx
000000000040155B  mov     [rbp+18h], rdx
000000000040155F  call    __main                        ;調用初始化函數
0000000000401564  cvtsi2ss xmm0, dword ptr [rbp+10h]
0000000000401569  movss   dword ptr [rbp-4], xmm0       ;f=(float)argc
000000000040156E  cvtss2sd xmm0, dword ptr [rbp-4]      ;xmm0=(double)f
0000000000401573  movq    rax, xmm0
0000000000401578  mov     rdx, rax
000000000040157B  movq    xmm1, rdx
0000000000401580  mov     rdx, rax                      ;參數2 (double)f
0000000000401583  lea     rcx, Format                   ;參數1 "%f"
000000000040158A  call    printf                        ;調用printf函數
000000000040158F  movss   xmm0, dword ptr [rbp-4]
0000000000401594  cvttss2si eax, xmm0
0000000000401598  mov     [rbp+10h], eax                ;argc=(int)f
000000000040159B  mov     edx, [rbp+10h]                ;參數1 argc
000000000040159E  lea     rcx, aD                       ;參數2 "%d"
00000000004015A5  call    printf                        ;調用printf函數
00000000004015AA  mov     eax, 0
00000000004015AF  add     rsp, 30h
00000000004015B3  pop     rbp
00000000004015B4  retn
//x64_clang對應匯編代碼講解
0000000140001000  sub     rsp, 48h
0000000140001004  mov     dword ptr [rsp+44h], 0
000000014000100C  mov     [rsp+38h], rdx                ;保存argv
0000000140001011  mov     [rsp+34h], ecx                ;保存argc
0000000140001015  mov     ecx, [rsp+34h]
0000000140001019  cvtsi2ss xmm0, ecx
000000014000101D  movss   dword ptr [rsp+30h], xmm0     ;f=(int)argc
0000000140001023  movss   xmm0, dword ptr [rsp+30h]
0000000140001029  cvtss2sd xmm0, xmm0                   ;xmm0=(double)f
000000014000102D  lea     rcx, asc_1400122C0            ;參數1 "%f"
0000000140001034  movaps  xmm1, xmm0
0000000140001037  movq    rdx, xmm0                     ;參數2 (double)f
000000014000103C  call    sub_140001070                 ;調用printf函數
0000000140001041  cvttss2si r8d, dword ptr [rsp+30h]
0000000140001048  mov     [rsp+34h], r8d                ;argc=(int)f
000000014000104D  mov     edx, [rsp+34h]                ;參數2 argc
0000000140001051  lea     rcx, aD                       ;參數1 "%d"
0000000140001058  mov     [rsp+2Ch], eax
000000014000105C  call    sub_140001070                 ;調用printf函數
0000000140001061  xor     edx, edx
0000000140001063  mov     [rsp+28h], eax
0000000140001067  mov     eax, edx
0000000140001069  add     rsp, 48h
000000014000106D  retn

代碼清單2-2通過浮點數與整數、整數與浮點數間的互相轉換演示了數據傳送類型的浮點指令的使用方法。從示例中可以發現,float類型的浮點數雖然占4字節,但是使用浮點棧將以8字節方式進行處理,而使用媒體寄存器則以4字節處理。當浮點數作為參數時,并不能直接壓棧,PUSH指令只能傳入4字節數據到棧中,這樣會丟失4字節數據。這就是使用printf函數以整數方式輸出浮點數時會產生錯誤的原因。printf以整數方式輸出時,將對應參數作為4字節數據長度,按補碼方式解釋,而真正壓入的參數為浮點類型時,卻是8字節長度,需要按浮點編碼方式解釋。

浮點數作為返回值的情況也是如此,在32位程序中使用浮點棧st(0)作為返回值同樣需要傳遞8字節數據,64位程序中使用媒體寄存器xmm0作為返回值只需要傳遞4字節,如代碼清單2-3所示。

代碼清單2-3 浮點數作為返回值

// C++源碼
#include <stdio.h>
float getFloat() {
  return 12.25f;
}

int main(int argc, char* argv[]) {
  float f = getFloat();
  return 0;
}
//x86_vs對應匯編代碼講解
00401010  push    ebp
00401011  mov     ebp, es
00401013  push    ecx                 ;參數1 argc入棧
00401014  call    sub_401000          ;調用getFloat()函數
00401019  fstp    dword ptr [ebp-4]   ;f=getFloat()將st(0)的雙精度數轉換為單精度數
0040101C  xor     eax, eax
0040101E  mov     esp, ebp
00401020  pop     ebp
00401021  retn

00401000  push    ebp
00401001  mov     ebp, esp
00401003  fld     ds:flt_40D150       ;將返回值入棧到st(0)中,單精度數轉換為雙精度數入棧
00401009  pop     ebp
0040100A  retn                        ;getFloat()函數返回

//x86_gcc對應匯編代碼講解
00401517  push    ebp
00401518  mov     ebp, esp
0040151A  and     esp, 0FFFFFFF0h     ;棧對齊
0040151D  sub     esp, 10h
00401520  call    ___main             ;調用初始化函數
00401525  call    __Z8getFloatv       ;調用getFloat()函數
0040152A  fstp    dword ptr [esp+0Ch] ;f=getFloat()將st(0)的雙精度數轉換為單精度數
0040152E  mov     eax, 0
00401533  leave
00401534  retn
00401510  fld     ds:flt_404000       ;將返回值入棧到st(0)中,單精度數轉換為雙精度數入棧
00401516  retn                        ;getFloat()函數返回

//x86_clang對應匯編代碼講解
00401010  push    ebp
00401011  mov     ebp, esp
00401013  sub     esp, 14h
00401016  mov     eax, [ebp+0Ch]      ;eax=argv
00401019  mov     ecx, [ebp+8]        ;ecx=argc
0040101C  mov     dword ptr [ebp-4], 0
00401023  mov     [ebp-10h], eax
00401026  mov     [ebp-14h], ecx
00401029  call    sub_401000          ;調用getFloat()函數
0040102E  fstp    dword ptr [ebp-0Ch] ;f=getFloat()將st(0)的雙精度數轉換為單精度數
00401031  movss   xmm0, dword ptr [ebp-0Ch]
00401036  xor     eax, eax
00401038  movss   dword ptr [ebp-8], xmm0
0040103D  add     esp, 14h
00401040  pop     ebp
00401041  retn

00401000  push    ebp
00401001  mov     ebp, esp
00401003  fld     ds:flt_40D150       ;將返回值入棧到st(0)中,單精度數轉換為雙精度數入棧
00401009  pop     ebp
0040100A  retn                        ;getFloat()函數返回

//x64_vs對應匯編代碼講解
0000000140001010  mov     [rsp+10h], rdx
0000000140001015  mov     [rsp+8], ecx
0000000140001019  sub     rsp, 38h
000000014000101D  call    sub_140001000 ;調用getFloat()函數
0000000140001022  movss   dword ptr [rsp+20h], xmm0;f=getFloat()從xmm0獲取返回值
0000000140001028  xor     eax, eax
000000014000102A  add     rsp, 38h
000000014000102E  retn

0000000140001000  movss   xmm0, cs:dword_14000D2C0;xmm0=12.25f
0000000140001008  retn                  ;getFloat()函數返回

//x64_gcc對應匯編代碼講解
0000000000401559  push    rbp
000000000040155A  mov     rbp, rsp
000000000040155D  sub     rsp, 30h
0000000000401561  mov     [rbp+10h], ecx
0000000000401564  mov     [rbp+18h], rdx
0000000000401568  call    __main      ;調用初始化函數
000000000040156D  call    _Z8getFloatv ;調用getFloat()函數
0000000000401572  movd    eax, xmm0
0000000000401576  mov     [rbp-4], eax ;f=getFloat()從xmm0獲取返回值
0000000000401579  mov     eax, 0
000000000040157E  add     rsp, 30h
0000000000401582  pop     rbp
0000000000401583  retn

0000000000401550  movss   xmm0, cs:dword_404000;xmm0=12.25f
0000000000401558  retn                ;getFloat()函數返回

//x64_clang對應匯編代碼講解
0000000140001010  sub     rsp, 38h
0000000140001014  mov     dword ptr [rsp+34h], 0
000000014000101C  mov     [rsp+28h], rdx
0000000140001021  mov     [rsp+24h], ecx
0000000140001025  call    sub_140001000 ; 調用getFloat()函數
000000014000102A  xor     eax, eax
000000014000102C  movss   dword ptr [rsp+20h], xmm0;f=getFloat()從xmm0獲取返回值
0000000140001032  add     rsp, 38h

0000000140001036  retn

0000000140001000  movss   xmm0, cs:dword_14000D2C0;xmm0=12.25f
0000000140001008  retn                  ;getFloat()函數返回
主站蜘蛛池模板: 泸水县| 儋州市| 贵阳市| 湖州市| 扎赉特旗| 通江县| 大英县| 民乐县| 桃园县| 宁强县| 绥江县| 永靖县| 衡山县| 汾阳市| 汉沽区| 永仁县| 绥宁县| 甘孜| 全州县| 中牟县| 正定县| 呼和浩特市| 洛南县| 肥城市| 乡城县| 孝昌县| 西乌珠穆沁旗| 牟定县| 平罗县| 乳源| 竹溪县| 德安县| 耒阳市| 子长县| 南宫市| 阿尔山市| 通河县| 莱芜市| 开封县| 临朐县| 郁南县|