久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

    深入代碼優化 (二) 使用SIMD優化程序

     lichwoo 2025-01-08 發布于中國香港

    在現代 CPU 中,并行性操作大致分為三種類型:

    (1)指令級并行,主要由 cpu 流水線技術,亂序執行技術等技術完成。

    (2)線程級并行,主要依靠多核多線程技術實現。

    (3)數據級并行,主要依靠 SIMD (單指令多數據) 來實現。

    指令級并行和線程級并行這兩種技術不在本文進行討論,本文將詳細介紹 SIMD 及其使用方法。

    SIMD 介紹

    SIMD 是 CPU 硬件設計的一部分,并且是可以通過指令集架構 (ISA) 直接訪問。SIMD 描述了具有多個處理元件的計算機同時對多個數據執行相同操作的過程。

    以加法指令為例,單指令單數據 (SISD) 的 CPU 對加法指令譯碼后,執行部件先訪問內存,取得第一個操作數,之后再一次訪問內存,取得第二個操作數,隨后才能進行求和運算。而在支持 SIMD 的 CPU 中,指令譯碼后幾個執行部件同時訪問內存,一次性獲得所有操作數進行運算。這個特點使 SIMD 特別適合于多媒體應用等數據密集型運算。

    英特爾的第一個 IA-32 SIMD 指令集是 MMX 指令集。它沿用了 x87 時代的浮點寄存器,使 CPU 無法對浮點數進行 SIMD 操作,只能處理整數。SIMD 流指令擴展 (SSE) 是 x86 架構的 SIMD 指令集擴展,最早是由 Intel 設計并在1999年推出的奔騰3系列 CPU 中引入。SSE 浮點指令在新的獨立寄存器 XMM 上運行,并擴展了一些在 MMX 寄存器上運行的整數指令。SSE 包含了70條新指令,其中大部分都適用于單精度浮點數據類型 (float) 。

    SSE最初添加了8個新的128位寄存器,XMM0 - XMM7,在 AMD64 拓展里面,又額外添加了8個寄存器,XMM8 - XMM15,這個拓展也被引入到了 Intel64 位處理器 (IA-64) 架構中。不過寄存器XMM8 - XMM15 只能用來處理 64bit 的操作數。SSE2 進一步支持雙精度浮點數,由于寄存器長度沒有變長,所以只能支持2個雙精度浮點計算或是4個單精度浮點計算。另外,它在這組寄存器上實現了整型計算,從而代替了 MMX。

    在 Intel 的 AVX 指令集中,將 SSE 的128位數據通道拓寬到256位,并由此產生的寄存器稱之為YMM。并且 AVX 全面兼容 SSE/SSE2/SSE3/SSE4,也就是 YMM 寄存器的低128位就是 XMM 寄存器。

    現代編譯器有三種方式來支持 SIMD:

    (1)編譯器能夠在沒有用戶干預的情況下生成 SIMD 代碼,稱之為自動矢量化。

    (2)用戶可以插入 Intrinsics 函數實現 SIMD。

    (3)用戶可以使用矢量 C++ 類 (僅限ICC編譯器) 來實現 SIMD。

    1. 自動矢量化

    在程序編寫過程中,可能會經常遇到以下循環的方式,一次執行許多數字的加法。

    1. for (i = 0; i < n; i ++)
    2. c[i] = a[i] + b[i];

    在 gcc 編譯器中,如果添加了 -ftree-vectorize 編譯選項 (O2已包含此優化選項),那么編譯器可以自動將這類循環轉換成矢量操作序列。

    下面以 vector.c 做實驗,看看編譯器怎么實現適量自動化。

    1. [root@wuhan ~]# cat vector.c
    2. #include <stdio.h>
    3. #define N 100000
    4. #define M 1024
    5. int a[M], b[M], c[M];
    6. int main()
    7. {
    8. int i, j;
    9. for (j = 0; j < N; j ++)
    10. for (i = 0; i < M; i ++)
    11. c[i] = a[i] + b[i];
    12. return 0;
    13. }

    對其進行編譯運行,第一次編譯不帶 -ftree-vectorize,即期望編譯器不對這段代碼進行自動矢量化。第二次帶上 -ftree-vectorize,期望編譯器能對其進行自動矢量化。由于 -ftree-vectorize 只在 -O1 優化下才生效,所以兩次編譯也都帶了 -O1 進行。

    1. [root@wuhan ~]# gcc vector.c -O1 -o no_vector
    2. [root@wuhan ~]# time ./no_vector
    3. real 0m0.110s
    4. user 0m0.108s
    5. sys 0m0.001s
    6. [root@wuhan ~]# gcc vector.c -O1 -o with_vector -ftree-vectorize
    7. [root@wuhan ~]# time ./with_vector
    8. real 0m0.029s
    9. user 0m0.028s
    10. sys 0m0.000s

    從運行耗時可以看出,矢量化編譯 (-ftree-vectorize) 之后性能大大提升。再將這段程序的匯編碼打印出來,可以看出矢量化編譯之后,匯編碼里面使用了 XMM 寄存器。前面介紹過了,XMM 寄存器可以同時裝載4個 int 類型的數據,并對其進行相同的操作,這也就是性能提升的關鍵。

    [root@wuhan ~]# gcc vector.c -O1 -S             
    [root@wuhan ~]# cat vector.s 
            .file   "vector.c"
            .text
            .globl  main
            .type   main, @function
    main:
    .LFB11:
            .cfi_startproc
            movl    $100000, %ecx
            jmp     .L2
    .L6:
            subl    $1, %ecx
            je      .L4
    .L2:
            movl    $0, %eax
    .L3:
            movl    b(%rax), %edx
            addl    a(%rax), %edx
            movl    %edx, c(%rax)
            addq    $4, %rax
            cmpq    $4096, %rax
            jne     .L3
            jmp     .L6
    .L4:
            movl    $0, %eax
            ret
            .cfi_endproc
    .LFE11:
            .size   main, .-main
            .comm   c,4096,32
            .comm   b,4096,32
            .comm   a,4096,32
            .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
            .section        .note.GNU-stack,"",@progbits

    [root@wuhan ~]# gcc vector.c -O1 -ftree-vectorize -S
    [root@wuhan ~]# cat vector.s 
            .file   "vector.c"
            .text
            .globl  main
            .type   main, @function
    main:
    .LFB11:
            .cfi_startproc
            movl    $100000, %edx
    .L2:
            movl    $0, %eax
    .L3:
           
    movdqa  a(%rax), %xmm0
            paddd   b(%rax), %xmm0
            movaps  %xmm0, c(%rax)
            addq    $16, %rax
            cmpq    $4096, %rax
            jne     .L3
            subl    $1, %edx
            jne     .L2
            movl    $0, %eax
            ret
            .cfi_endproc
    .LFE11:
            .size   main, .-main
            .comm   c,4096,32
            .comm   b,4096,32
            .comm   a,4096,32
            .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
            .section        .note.GNU-stack,"",@progbits

    上面的代碼很容易自動矢量化,我們再來對比一下這兩段代碼:

    #include <stdio.h>

    #define N 100000
    #define M 1024
    int a[M+16], b[M], c[M];

    int main()
    {
        int i, j;
        for (j = 0; j < N; j ++)
            for (i = 0; i < M; i ++)
            {
                a[i+1] = a[i];
            }
        return 0;
    }

    #include <stdio.h>

    #define N 100000
    #define M 1024
    int a[M+16], b[M], c[M];

    int main()
    {
        int i, j;
        for (j = 0; j < N; j ++)
            for (i = 0; i < M; i ++)
            {
                a[i+4] = a[i];
            }
        return 0;
    }

    區別之處已加粗顯示,分別將他們編成匯編碼,再進行對比:

    [root@wuhan ~]# gcc vector2.c -O1 -ftree-vectorize -S

    [root@wuhan ~]# cat vector2.s 
            .file   "vector2.c"
            .text
            .globl  main
            .type   main, @function
    main:
    .LFB11:
            .cfi_startproc
            movl    $100000, %esi
            movl    $a+4096, %ecx
            jmp     .L2
    .L6:
            subl    $1, %esi
            je      .L4
    .L2:
            movl    $a, %eax
    .L3:
            movl    (%rax), %edx
            movl    %edx, 4(%rax)
            addq    $4, %rax
            cmpq    %rax, %rcx
            jne     .L3
            jmp     .L6
    .L4:
            movl    $0, %eax
            ret
            .cfi_endproc
    .LFE11:
            .size   main, .-main
            .comm   c,4096,32
            .comm   b,4096,32
            .comm   a,4160,32
            .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
            .section        .note.GNU-stack,"",@progbits

    [root@wuhan ~]# gcc vector2.c -O1 -ftree-vectorize -S

    [root@wuhan ~]# cat vector2.s 
            .file   "vector2.c"
            .text
            .globl  main
            .type   main, @function
    main:
    .LFB11:
            .cfi_startproc
            movl    $100000, %ecx
            movl    $a+4096, %edx
    .L2:
            movl    $a, %eax
    .L3:
           
    movdqa  (%rax), %xmm0
            movaps  %xmm0, 16(%rax)
            addq    $16, %rax
            cmpq    %rax, %rdx
            jne     .L3
            subl    $1, %ecx
            jne     .L2
            movl    $0, %eax
            ret
            .cfi_endproc
    .LFE11:
            .size   main, .-main
            .comm   c,4096,32
            .comm   b,4096,32
            .comm   a,4160,32
            .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
            .section        .note.GNU-stack,"",@progbits

    顯然右側程序的匯編碼使用了 XMM 寄存器,而左邊的程序卻沒有。

    假如他們都會進行矢量化,那么以下4條操作是要同時進行的,假如 a[0] = 0, a[1] = 1, a[2] = 2...,那么左邊的程序運行完之后,得到的結果 a[1] = 0, a[2] = 1, a[3] = 2...。但實際上,左邊程序運行完之后應該得到的結果是 a[1] = 0, a[2] = 0, a[3] = 0...。所以,如果左邊的程序也矢量化,那么程序的結果就是錯誤的。而右邊的程序卻不受影響,雖然右邊程序  a[i+4] 的值也依賴于  a[i],但是他們地址相差128位,而XMM寄存器剛好是128位寬,矢量化運行之后也不影響本來的結果。

    a[1] = a[0];

    a[2] = a[1];

    a[3] = a[2];

    a[4] = a[3];

    a[4] = a[0];

    a[5] = a[1];

    a[6] = a[2];

    a[7] = a[3];

    自動矢量化,就像任何循環優化或其他編譯優化一樣,必須準確地保留程序行為。在執行期間必須遵守所有的依賴項,以防止出現錯誤結果。如果出現處理不了的循環依賴,那么循環依賴必須獨立于矢量化指令執行。

    要矢量化一個程序,編譯器必須首先了解語句之間的依賴關系,并在必要時重新對齊它們。 一旦映射了依賴關系,編譯器必須正確安排實現指令,將適當的候選者更改為矢量指令,并用這些指令對多個數據項進行操作。

    編譯器進行矢量自動化優化通常會經歷一下三個步驟:

    (1)建立依賴圖。識別哪些語句依賴于哪些其他語句。這包括檢查每個語句并識別語句訪問的每個數據項,將數組訪問修飾符映射到函數以及檢查每個訪問對所有語句中其他訪問的依賴關系。依賴圖包含了距離不大于矢量大小的所有局部依賴。 如果矢量寄存器為 128 位,數組類型為 32 位,則矢量大小為 128/32 = 4。所有其他非循環依賴項都不應使其矢量化無效,因為他們不會調用相同的矢量指令。

    (2)聚類。使用依賴圖,優化器可以對強連接組件 (SCC) 進行聚類,并將可矢量化語句與其余語句分開。例如,一個程序的循環內包含三個語句組:(SCC1+SCC2)、SCC3 和 SCC4,其中只有第二組 (SCC3) 可以矢量化。那么最終的程序將包含三個循環,每個循環一個語句組,只有中間的循環被矢量化。 優化器不能在不違反語句執行順序的情況下將第一個與最后一個連接起來,因為這很可能會保證不了數據有效性。

    (3)監測慣用語法,一些不明顯的依賴可以根據特定的習慣用法進一步優化。例如,下面的數據依賴項可以進行矢量化,因為可以獲取右側值然后將其存儲在左側值上,因此數據不會在賦值中發生變化。

    a[i] = a[i+1];  /* 不同于a[i+1] = a[i], a[i+1] = a[i]不能矢量化 */

    2. 插入 Intrinsics 函數 (內在函數) 實現 SIMD

    對于程序員來說,intrinsics 看起來就像普通的庫函數。只要包含了相關的頭文件,就可以使用內在函數。如果要將4個整數和另外4個整數相加,可以使用 _mm_add_epi32 內在函數。這個函數的聲明包含在 <emmintrin.h> 頭文件中。

    intrinsics 與庫函數不同的是,intrinsics 是直接在編譯器中實現的。上面的 _mm_add_epi32 SSE2內在函數通常編譯成一條指令 paddd。__m128i 內置數據類型是四個整數型的矢量,每個 32 位,總共 128 位。編譯器將發出兩條指令:第一條將參數從內存加載到寄存器中,第二條將四個值相加。通常來說,CPU 調用一個庫函數所花費的時間,可能是調用 intrinsics 的數倍。

    包含足夠數量的矢量  intrinsics 或嵌入等效匯編源代碼的過程稱為手動矢量化。現代編譯器和庫已經使用內在函數、匯編或兩者的組合實現了很多東西。例如,memset,memcpy 或 memmove 標準 C 庫函數的實現就使用 SSE2 指令以獲得更好的性能。然而,在高性能計算、游戲開發或編譯器開發等細分領域之外,即使是非常有經驗的 C 和 C++ 程序員在很大程度上也不熟悉 SIMD 內在函數。

    下面函數是使用 intrinsics 函數來實現 c[i] = a[i] + b[i] 。

    1. [root@wuhan ~]# cat vector_sse2.c
    2. #include <stdio.h>
    3. #include <emmintrin.h>
    4. #define N 100000
    5. #define M 1024
    6. int a[M], b[M], c[M];
    7. int add_sse2(int size, int *a, int *b, int *c) {
    8. int i = 0;
    9. for (; i + 4 <= size; i += 4) {
    10. /* 加載 a, b 數組的 128 位塊 */
    11. __m128i ma = _mm_loadu_si128((__m128i*) &a[i]);
    12. __m128i mb = _mm_loadu_si128((__m128i*) &b[i]);
    13. /* 128 位塊矢量相加 */
    14. ma = _mm_add_epi32(ma, mb);
    15. /* 將相加結果存儲到 c 數組的 128 位塊 */
    16. _mm_storeu_si128((__m128i*) &c[i], ma);
    17. }
    18. }
    19. int main()
    20. {
    21. for (int i = 0; i < N; i ++)
    22. add_sse2(M, a, b, c);
    23. return 0;
    24. }

     將文件編成匯編碼,匯編碼包含了 paddd   %xmm1, %xmm0

    1. [root@wuhan ~]# gcc vector_sse2.c -O1 -S
    2. [root@wuhan ~]# cat vector_sse2.s
    3. .file "vector_sse2.c"
    4. .text
    5. .comm a,4160,32
    6. .comm b,4096,32
    7. .comm c,4096,32
    8. .globl add_sse2
    9. .type add_sse2, @function
    10. add_sse2:
    11. .LFB503:
    12. .cfi_startproc
    13. pushq %rbp
    14. .cfi_def_cfa_offset 16
    15. .cfi_offset 6, -16
    16. movq %rsp, %rbp
    17. .cfi_def_cfa_register 6
    18. subq $40, %rsp
    19. movl %edi, -132(%rbp)
    20. movq %rsi, -144(%rbp)
    21. movq %rdx, -152(%rbp)
    22. movq %rcx, -160(%rbp)
    23. movl $0, -4(%rbp)
    24. jmp .L2
    25. .L6:
    26. movl -4(%rbp), %eax
    27. cltq
    28. leaq 0(,%rax,4), %rdx
    29. movq -144(%rbp), %rax
    30. addq %rdx, %rax
    31. movq %rax, -128(%rbp)
    32. movq -128(%rbp), %rax
    33. movdqu (%rax), %xmm0
    34. movaps %xmm0, -32(%rbp)
    35. movl -4(%rbp), %eax
    36. cltq
    37. leaq 0(,%rax,4), %rdx
    38. movq -152(%rbp), %rax
    39. addq %rdx, %rax
    40. movq %rax, -120(%rbp)
    41. movq -120(%rbp), %rax
    42. movdqu (%rax), %xmm0
    43. movaps %xmm0, -48(%rbp)
    44. movdqa -32(%rbp), %xmm0
    45. movaps %xmm0, -96(%rbp)
    46. movdqa -48(%rbp), %xmm0
    47. movaps %xmm0, -112(%rbp)
    48. movdqa -96(%rbp), %xmm1
    49. movdqa -112(%rbp), %xmm0
    50. paddd %xmm1, %xmm0
    51. movaps %xmm0, -32(%rbp)
    52. movl -4(%rbp), %eax
    53. cltq
    54. leaq 0(,%rax,4), %rdx
    55. movq -160(%rbp), %rax
    56. addq %rdx, %rax
    57. movq %rax, -56(%rbp)
    58. movdqa -32(%rbp), %xmm0
    59. movaps %xmm0, -80(%rbp)
    60. movdqa -80(%rbp), %xmm0
    61. movq -56(%rbp), %rax
    62. movups %xmm0, (%rax)
    63. addl $4, -4(%rbp)
    64. .L2:
    65. movl -4(%rbp), %eax
    66. addl $3, %eax
    67. cmpl %eax, -132(%rbp)
    68. jg .L6
    69. nop
    70. leave
    71. .cfi_def_cfa 7, 8
    72. ret
    73. .cfi_endproc
    74. .LFE503:
    75. .size add_sse2, .-add_sse2
    76. .globl main
    77. .type main, @function
    78. main:
    79. .LFB504:
    80. .cfi_startproc
    81. pushq %rbp
    82. .cfi_def_cfa_offset 16
    83. .cfi_offset 6, -16
    84. movq %rsp, %rbp
    85. .cfi_def_cfa_register 6
    86. subq $16, %rsp
    87. movl $0, -4(%rbp)
    88. jmp .L8
    89. .L9:
    90. movl $c, %ecx
    91. movl $b, %edx
    92. movl $a, %esi
    93. movl $1024, %edi
    94. call add_sse2
    95. addl $1, -4(%rbp)
    96. .L8:
    97. cmpl $99999, -4(%rbp)
    98. jle .L9
    99. movl $0, %eax
    100. leave
    101. .cfi_def_cfa 7, 8
    102. ret
    103. .cfi_endproc
    104. .LFE504:
    105. .size main, .-main
    106. .ident "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
    107. .section .note.GNU-stack,"",@progbits

    測試一下這個程序的性能, 比前面介紹的直接使用矢量化優化的程序性能還要好一些。

    [root@wuhan ~]# gcc vector.c -O1 -o with_vector -ftree-vectorize
    [root@wuhan ~]# time ./with_vector 

    real    0m0.029s
    user    0m0.028s
    sys     0m0.000s

    [root@wuhan ~]# gcc vector_sse2.c -o vector_sse2 -O1
    [root@wuhan ~]# time ./vector_sse2 

    real    0m0.020s
    user    0m0.020s
    sys     0m0.000s

    3. 利用 C++ 類 (ICC編譯器專用)

    使用該方法依賴于環境里面安裝了 Intel ICC 編譯器。ICC 編譯器集成在 Intel oneAPI 開發套件里面,下載鏈接:Download the Intel? oneAPI Base Toolkit

    直接在 linux 上用 yum 安裝:

    (1)在 /temp 文件夾底下創建 YUM 的 repo 文件

    1. tee > /tmp/oneAPI.repo << EOF
    2. [oneAPI]
    3. name=Intel? oneAPI repository
    4. baseurl=https://yum.repos.intel.com/oneapi
    5. enabled=1
    6. gpgcheck=1
    7. repo_gpgcheck=1
    8. gpgkey=https://yum.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB
    9. EOF

    (2)將新建的 repo 文件移到 /etc/yum.repos.d

    sudo mv /tmp/oneAPI.repo /etc/yum.repos.d

    (3)使用 yum 命令安裝 Intel? oneAPI Base Toolkit

    sudo yum install intel-basekit

    (4)在使用編譯器之前運行setvars.sh設置環境變量

    source /opt/intel/oneapi/setvars.sh intel64

    (5)安裝結束,查看編譯器版本

    1. [root@wuhan ~]# icpx --version
    2. Intel(R) oneAPI DPC++/C++ Compiler 2021.4.0 (2021.4.0.20210924)
    3. Target: x86_64-unknown-linux-gnu
    4. Thread model: posix
    5. InstalledDir: /opt/intel/oneapi/compiler/2021.4.0/linux/bin

    使用 C++ 類進行 SIMD 操作允許在單個操作中對數組或數據向量進行操作。同樣是計算 c[i] = a[i] + b[i],用傳統數組的方法表示如下:

    1. int a[4], b[4], c[4];
    2. for (i=0; i<4; i++) /* 需要4次迭代 */
    3. c[i] = a[i] + b[i]; /* 分別計算 c[0], c[1], c[2], c[3] */

    在 ICC 編譯器中可以使用 Ivec 類來表示 (需要添加頭文件 dvec.h)

    1. Is32vec4 ivecA, ivecB, ivecC; /* 只需要一次迭代 */
    2. ivecC = ivecA + ivecB; /* 計算 ivecC0, ivecC1, ivecC2, ivecC3 */

    所以可以把前面示例的程序改造如下:

    1. [root@wuhan ~]# cat vector_class.cpp
    2. #include <iostream>
    3. #include <dvec.h>
    4. using namespace std;
    5. #define N 100000
    6. #define M 1024
    7. Is32vec4 a[M/4], b[M/4], c[M/4];
    8. int main()
    9. {
    10. for (int i = 0; i < N; i ++)
    11. for (int j = 0; j < M/4; j ++)
    12. c[j] = a[j] + b[j];
    13. return 0;
    14. }

    把程序變成匯編碼,檢查一下是否用了 XMM 寄存器,C++ 匯編碼編譯出來太長了,就不貼全部源碼。果然 paddd 指令和 XMM 寄存器也都使用了。

    1. [root@wuhan ~]# icpx vector_class.cpp -O1 -S
    2. [root@wuhan ~]# cat vector_class.s | grep -i xmm
    3. movaps %xmm0, c(%rbx)
    4. movdqa %xmm0, (%rsp) # 16-byte Spill
    5. paddd (%rsp), %xmm0 # 16-byte Folded Reload
    6. movaps 16(%rsp), %xmm0
    7. movaps (%rdi), %xmm0
    8. movaps %xmm0, (%rdi)

    總結

    SIMD 的使用不是那么簡單,一般程序員也不太會使用 Intrinsics 函數或者 Ivec 類來優化  SIMD,基本上都是靠編譯器幫我們進行自動矢量化。

    想要代碼能盡量的自動矢量化,以下幾點其實是我們可以做到的:

    - 避免使用全局指針和全局變量以幫助編譯器生成 SIMD 代碼。

    - 使用盡可能小的 SIMD 數據類型,通過使用更長的 SIMD 矢量長度來實現更多的并行性。

    - 合理安排循環的嵌套,以便最內層的嵌套沒有迭代間的依賴關系。尤其要避免在較早的迭代中存儲數據,而在往后的迭代中加載該數據。

    - 避免在循環內使用條件分支。

    - 保持循環變量表達式簡單。

    參考文獻:

    《64-ia-32-architectures-optimization-manual》

    https://www.intel.com/content/www/us/en/develop/documentation/oneapi-dpcpp-cpp-compiler-dev-guide-and-reference/top/compiler-reference/libraries/intel-c-class-libraries/c-classes-and-simd-operations.html

    https://en./wiki/Streaming_SIMD_Extensions

    Intel? Intrinsics Guide

    CS3330: A quick guide to SSE/SIMD

    https://www.eidos.ic.i./~tau/lecture/parallel_distributed/2018/slides/pdf/simd2.pdf

    Basics of SIMD Programming

    Improving performance with SIMD intrinsics in three use cases - Stack Overflow Blog

    https://en./wiki/Automatic_vectorization

    SIMD指令學習筆記 - 濁酒戀紅塵 - 博客園

      本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發現有害或侵權內容,請點擊一鍵舉報。
      轉藏 分享 獻花(0

      0條評論

      發表

      請遵守用戶 評論公約

      類似文章 更多

      主站蜘蛛池模板: 色窝窝免费播放视频在线| 亚洲乱码中文字幕小综合| 亚洲伊人久久综合影院| 亚洲中文久久久精品无码| 亚洲一区二区精品极品| 亚洲丰满熟女一区二区蜜桃| 日本欧美一区二区三区在线播放 | 人妻丰满熟AV无码区HD| 无码日韩精品一区二区三区免费| GV无码免费无禁网站男男| 国产精品久久毛片| 国产不卡精品视频男人的天堂| 国产精品视频第一区二区三区| 亚洲精品一区二区美女| 欧洲美熟女乱又伦AV影片| 亚洲欧美日韩愉拍自拍美利坚| 国产精品自在线拍国产电影| 国产午夜精品福利91| 日韩精品久久久久久久电影蜜臀| 亚洲日本欧洲二区精品| 老熟妇性色老熟妇性| 欧美不卡无线在线一二三区观 | 国产成人a在线观看视频免费 | 老司机67194精品线观看| 成人国产精品中文字幕| 久久人搡人人玩人妻精品首页 | 国产不卡一区二区精品| 一本色道久久88亚洲精品综合| 午夜成人无码免费看网站| 八区精品色欲人妻综合网| 亚洲AV熟妇在线观看| 亚洲理论在线A中文字幕| 久久久综合香蕉尹人综合网| 强开少妇嫩苞又嫩又紧九色| 中文字幕亚洲精品人妻| 爆乳无码AV一区二区三区| 亚洲WWW永久成人网站| 国产超高清麻豆精品传媒麻豆精品| 亚洲中文字幕久久精品无码喷水| 欧美人与动欧交视频| 桃花岛亚洲成在人线AV|