webassembly006 SIMD 矢量运算

这篇具有很好参考价值的文章主要介绍了webassembly006 SIMD 矢量运算。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

        提高wasm性能的途径包括 : 编译期优化标志,使用多线程,使用SIMD内在函数()和wasm_webgpu等 SIMD是其中一个方向。

SIMD

         SIMD(单指令多数据流)即一条指令可以一次处理多个数据,属于数据级并行优化手段。SIMD在X86、ARM CPU架构下都有相应的指令集实现。Flynn分类法根据指令和数据进入CPU的方式,将计算机架构分为四种不同的类型

  • 单指令流单数据流(SISD, Single Instruction stream Single Data stream)
  • 单指令流多数据流(SIMD, Single Instruction stream Multiple Data stream)
  • 多指令流单数据流(MISD, Multiple Instruction stream Single Data stream)
  • 多指令流多数据流(MIMD, Multiple Instruction stream Multiple Data stream)
Data Stream
Single Multiple
Instruction
  Stream
Single SISD

a_1+b_1

SIMD

a_1+b_1
a_2+b_2
a_3+b_3
Multiple SIMT

a_1+b_1
a_1*b_1
a_1-b_1
MIMD

a_1+b_1
a_2*b_2
a_3-b_3

SIMD Elements per Data Type

                由于通常没有内建的128bit和256bit数据类型,SIMD指令使用自己构建的数据类型,这些类型以union实现,这些数据类型可以称作向量,一般来说,MMX指令是__m64 类型的数据,SSE是__m128类型(一次操作128bit数据,128bit = 16byte = 4*float)的数据等等。

char short int int64_t float double
MMX 8 4 2 1 0 0
SSE 0 0 0 0 4 0
SSE2 16 8 4 2 4 2
AVX 16 8 4 2 8 4
AVX2 32 16 8 4 8 4
AVX512F 32 16 16 8 16 8
AVX512BW 64 32 16 8 16 8
Data type Intrinsic prefex
SSE
__m128 (float) __m128d (double) __m128i (int)
_mm_
AVX __m256 (float) __m256d (double) __m256i (int) _mm256_
AVX512 __m512 (float) __m512d (double) __m512i (int) _mm512_

函数指南

  • https://chryswoods.com/vector_c++/immintrin.html

  • x86/x64 SIMD Instruction List (SSE to AVX512)

  • 有关每个 SSE 内在函数的详细信息,请访问优秀的SSE1 英特尔内在函数指南。

  • www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

  • WebAssembly中simd使用调研

x86 SIMD 内在函数的头文件 immintrin.h

  • 当前,通常可以只使用<immintrin.h>. 它包括一切。
  • 从历史上看(在immintrin.h拉入所有内容之前),您必须手动包含所需的最高级别内在函数的标头。这对于 MSVC 和 ICC 仍然有用,可以阻止您使用不需要的指令集。
<mmintrin.h>  MMX
<xmmintrin.h> SSE
<emmintrin.h> SSE2
<pmmintrin.h> SSE3
<tmmintrin.h> SSSE3
<smmintrin.h> SSE4.1
<nmmintrin.h> SSE4.2
<ammintrin.h> SSE4A
<wmmintrin.h> AES
<immintrin.h> AVX, AVX2, FMA

例子

float input1[4] = { 1.2f, 3.5f, 1.7f, 2.8f };
float input2[4] = { -0.7f, 2.6f, 3.3f, -0.8f };
float output[4];
__m128 a = _mm_load_ps(input1);// https://www.csie.ntu.edu.tw/~r89004/hive/sse/page_4.html 要注意的是,這裡是假設這兩個浮點數陣列都是對齊在 16 bytes 的邊上。如果不是的話,就不能用 _mm_load_ps 這個 intrinsic 來載入,而要改用 _mm_loadu_ps 這個 intrinsic。它是專門用來處理沒有對齊在 16 bytes 邊上的資料的。但是,它的速度會比較慢。
__m128 b = _mm_load_ps(input2);
__m128 t = _mm_add_ps(a, b);
_mm_store_ps(output, t);

另外,因為 x86 的 little endian 特性,位址較低的 byte 會放在暫存器的右邊。也就是說,若以上面的 input1 為例,在載入到 XMM 暫存器後,暫存器中的 DATA0 會是 1.2,而 DATA1 是 3.5,DATA2 是 1.7,DATA3 是 2.8。如果需要以相反的順序載入的話,可以用 _mm_loadr_ps 這個 intrinsic。當然,在這個例子中,順序並不影響結果,所以不需要利用這個 intrinsic。

一般來說,宣告一個 float 的陣列,並不會對齊在 16 bytes 的邊上。如果希望它會對齊在 16 bytes 的邊上,以便利用 SSE 指令的話,Visual C++ 6.0 Processor Pack 和 Intel C++ compiler 都可以指定對齊方式。指定的方式如下:

__declspec(align(16)) float input1[4];

WebAssembly+SIMD

  • https://emscripten.org/docs/porting/simd.html

  • 目前仅支持 SSE1、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2 和 128 位 AVX 指令集。

  • 使用 WebAssembly LLVM 后端时,Emscripten 支持WebAssembly SIMD 建议。要启用 SIMD,请在编译时传递 -msimd128 标志。这还将打开 LLVM 的自动矢量化通道,因此无需修改源代码即可从 SIMD 中受益。

  • 在源代码级别,可以使用GCC/Clang SIMD 矢量扩展,并将在可能的情况下降低为 WebAssembly SIMD 指令。此外,还有一个可以使用的可移植内在函数头文件#include <wasm_simd128.h>

  • 内在函数头的单独文档正在开发中,但其用法很简单,其源代码可以在wasm_simd128.h中找到。这些内在函数正在与 SIMD 提案同时积极开发,不应被视为比提案本身更稳定。请注意,大多数引擎还需要一个额外的标志来启用 SIMD。例如,Node 需要–experimental-wasm-simd。

  • Emscripten 支持通过将-msse指令传递给编译器并包含头文件<xmmintrin.h>来编译使用 x86 SSE 的现有代码库(编译针对 x86 SSE 指令集的 SIMD 代码)。

code

1.编译针对 x86 SSE 指令集的 SIMD 代码

//  将两个数组a和b的元素逐个相乘,并将结果存储在数组c中。函数使用了AVX指令集中的256位浮点数寄存器__m256来进行向量化计算
void multiply(void) {
    unsigned i;
    __m256 A, B, C;

    for(i=0; i<(N & ((~(unsigned)0x7))); i+=8) {// 循环的终止条件是i小于数组长度N并且i是8的倍数,这是因为AVX指令集中的256位浮点数寄存器可以同时处理8个单精度浮点数。
        A = _mm256_load_ps(&a[i]);
        B = _mm256_load_ps(&b[i]);
        C = _mm256_mul_ps(A, B);
        _mm256_store_ps(&c[i], C);// 使用_mm256_store_ps函数将寄存器C中的结果存储回数组c
    }
    for(; i<N; i++) {// 如果数组长度N不是8的倍数,那么循环结束后还会有一些元素没有被处理到。因此,函数使用另一个循环来处理剩下的元素。在这个循环中,函数直接将数组a和b中对应位置的元素相乘,并将结果存储在数组c中。
        c[i] = a[i] * b[i];
    }
}
  • 完整代码
// https://juejin.cn/post/7091571543239000078
// emcc main.c -s ALLOW_MEMORY_GROWTH=1 -Os   -msimd128   -msse   -D USE_SSE    -o wasm_sse_os.html
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "sys/time.h"

// simd内置函数 头文件
#include <immintrin.h>



#define N           178257920 // 170M
#define SEED        0x1234


float *a, *b, *c;


#if defined(NORMAL)
// 为3个float数组分配内存,每个数组包含 170 * 1024 * 1024 个元素
void gen_data(void) {
    unsigned i;
    a = (float*) malloc(N*sizeof(float));
    b = (float*) malloc(N*sizeof(float));
    c = (float*) malloc(N*sizeof(float));
    
    srand(SEED);
    for(i=0; i<N; i++) {
        a[i] = b[i] = (float)(rand() % N);
    }
}

void free_data(void) {
    free(a);
    free(b);
    free(c);
}

void multiply(void) {
    unsigned i;
    for(i=0; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

#elif defined(USE_SSE)

void gen_data(void) {
    unsigned i;
    a = (float*) _mm_malloc(N*sizeof(float), 16);
    b = (float*) _mm_malloc(N*sizeof(float), 16);
    c = (float*) _mm_malloc(N*sizeof(float), 16);
    
    srand(SEED);
    for(i=0; i<N; i++) {
        a[i] = b[i] = (float)(rand() % N);
    }
}

void free_data(void) {
    _mm_free(a);
    _mm_free(b);
    _mm_free(c);
}

void multiply(void) {
    unsigned i;
    __m128 A, B, C; // 向量类型 __m128 = 4xfloat

    for(i=0; i<(N & ((~(unsigned)0x3))); i+=4) {
        A = _mm_load_ps(&a[i]);
        B = _mm_load_ps(&b[i]);
        C = _mm_mul_ps(A, B);
        _mm_store_ps(&c[i], C);
    }
    for(; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

#elif defined(USE_AVX)
void gen_data(void) {
    unsigned i;
    a = (float*) _mm_malloc(N*sizeof(float), 32);
    b = (float*) _mm_malloc(N*sizeof(float), 32);
    c = (float*) _mm_malloc(N*sizeof(float), 32);
    
    srand(SEED);
    for(i=0; i<N; i++) {
        a[i] = b[i] = (float)(rand() % N);
    }
}

void free_data(void) {
    _mm_free(a);
    _mm_free(b);
    _mm_free(c);
}

//  将两个数组a和b的元素逐个相乘,并将结果存储在数组c中。函数使用了AVX指令集中的256位浮点数寄存器__m256来进行向量化计算
void multiply(void) {
    unsigned i;
    __m256 A, B, C;

    for(i=0; i<(N & ((~(unsigned)0x7))); i+=8) {// 循环的终止条件是i小于数组长度N并且i是8的倍数,这是因为AVX指令集中的256位浮点数寄存器可以同时处理8个单精度浮点数。
        A = _mm256_load_ps(&a[i]);
        B = _mm256_load_ps(&b[i]);
        C = _mm256_mul_ps(A, B);
        _mm256_store_ps(&c[i], C);// 使用_mm256_store_ps函数将寄存器C中的结果存储回数组c
    }
    for(; i<N; i++) {// 如果数组长度N不是8的倍数,那么循环结束后还会有一些元素没有被处理到。因此,函数使用另一个循环来处理剩下的元素。在这个循环中,函数直接将数组a和b中对应位置的元素相乘,并将结果存储在数组c中。
        c[i] = a[i] * b[i];
    }
}
#endif


void print_data(void) {
    printf("%f, %f, %f, %f\n", c[0], c[1], c[N-2], c[N-1]);
}

gettimeofday();


int main(void) {
    double start=0.0, stop=0.0, msecs;
    struct timeval before, after;
    printf("gen data start... \n");
    gen_data(); 
    printf("gen data end... \n");

    gettimeofday(&before, NULL);
    multiply();
    gettimeofday(&after, NULL);

    msecs = (after.tv_sec - before.tv_sec)*1000.0 + (after.tv_usec - before.tv_usec)/1000.0;
    print_data();
    printf("Execution time = %2.3lf ms\n", msecs);

    free_data();
    return 0;
}

2.使用<wasm_simd128.h>方式

  • https://jeromewu.github.io/improving-performance-using-webassembly-simd-intrinsics/
#include<stdio.h>
#include<time.h>
#include <wasm_simd128.h>

void multiply_mats(int* out, int* in_a, int* in_b, int n) {
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			out[i*n+j] = 0;
      int sum_arr[] = {0, 0, 0, 0};
      v128_t sum = wasm_v128_load(sum_arr);
			for (int k = 0; k < n; k+=4) {
				v128_t a = wasm_v128_load(&in_a[i*n+k]);
				v128_t b = wasm_v128_load(&in_b[j*n+k]);
				v128_t prod = wasm_i32x4_mul(a, b);
        sum = wasm_i32x4_add(sum, prod);
			}
      v128_t sum_duo = wasm_i32x4_add(sum, wasm_i32x4_shuffle(sum, sum, 2, 3, 0, 0));
      v128_t sum_one = wasm_i32x4_add(sum_duo, wasm_i32x4_shuffle(sum_duo, sum_duo, 1, 0, 0, 0));
      out[i*n+j] += wasm_i32x4_extract_lane(sum_one, 0);
		}
	}
}
$:~/ggml/ggml/examples/mnist/web$ emcc new.cpp -o new.js
new.cpp:10:20: error: always_inline function 'wasm_v128_load' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   10 |       v128_t sum = wasm_v128_load(sum_arr);
      |                    ^
new.cpp:12:16: error: always_inline function 'wasm_v128_load' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   12 |                                 v128_t a = wasm_v128_load(&in_a[i*n+k]);
      |                                            ^
new.cpp:13:16: error: always_inline function 'wasm_v128_load' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   13 |                                 v128_t b = wasm_v128_load(&in_b[j*n+k]);
      |                                            ^
new.cpp:14:19: error: always_inline function 'wasm_i32x4_mul' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   14 |                                 v128_t prod = wasm_i32x4_mul(a, b);
      |                                               ^
new.cpp:15:15: error: always_inline function 'wasm_i32x4_add' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   15 |         sum = wasm_i32x4_add(sum, prod);
      |               ^
new.cpp:17:44: error: '__builtin_wasm_shuffle_i8x16' needs target feature simd128
   17 |       v128_t sum_duo = wasm_i32x4_add(sum, wasm_i32x4_shuffle(sum, sum, 2, 3, 0, 0));
      |                                            ^
/home/pdd/Downloads/emsdk/upstream/lib/clang/17/include/wasm_simd128.h:1445:12: note: expanded from macro 'wasm_i32x4_shuffle'
 1445 |   ((v128_t)__builtin_wasm_shuffle_i8x16(                                       \
      |            ^
new.cpp:17:24: error: always_inline function 'wasm_i32x4_add' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   17 |       v128_t sum_duo = wasm_i32x4_add(sum, wasm_i32x4_shuffle(sum, sum, 2, 3, 0, 0));
      |                        ^
new.cpp:18:48: error: '__builtin_wasm_shuffle_i8x16' needs target feature simd128
   18 |       v128_t sum_one = wasm_i32x4_add(sum_duo, wasm_i32x4_shuffle(sum_duo, sum_duo, 1, 0, 0, 0));
      |                                                ^
/home/pdd/Downloads/emsdk/upstream/lib/clang/17/include/wasm_simd128.h:1445:12: note: expanded from macro 'wasm_i32x4_shuffle'
 1445 |   ((v128_t)__builtin_wasm_shuffle_i8x16(                                       \
      |            ^
new.cpp:18:24: error: always_inline function 'wasm_i32x4_add' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   18 |       v128_t sum_one = wasm_i32x4_add(sum_duo, wasm_i32x4_shuffle(sum_duo, sum_duo, 1, 0, 0, 0));
      |                        ^
new.cpp:19:21: error: always_inline function 'wasm_i32x4_extract_lane' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   19 |       out[i*n+j] += wasm_i32x4_extract_lane(sum_one, 0);
      |                     ^
10 errors generated.
emcc: error: '/home/pdd/Downloads/emsdk/upstream/bin/clang++ -target wasm32-unknown-emscripten -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -DEMSCRIPTEN --sysroot=/home/pdd/Downloads/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat new.cpp -c -o /tmp/emscripten_temp_28ovmxxx/new_0.o' failed (returned 1)
$:~/ggml/ggml/examples/mnist/web$ emcc new.cpp -o new.js -msimd128
$:~/ggml/ggml/examples/mnist/web$ node new.js
multiply matrixs: 3.378s
$:~/ggml/ggml/examples/mnist/web$ emcc new.cpp -o new.js -O3 -msimd128
cache:INFO: generating system asset: symbol_lists/e0d283f526f4ef916e7ac46fde880e1f497e1610.json... (this will be cached in "/home/pdd/Downloads/emsdk/upstream/emscripten/cache/symbol_lists/e0d283f526f4ef916e7ac46fde880e1f497e1610.json" for subsequent builds)
cache:INFO:  - ok
$:~/ggml/ggml/examples/mnist/web$ node new.js
multiply matrixs: 0.338s

限制和行为差异

  • Emscripten 不支持 x86 或任何其他本机内联 SIMD 程序集或构建 .s 程序集文件,因此应编写所有代码以使用 SIMD 内部函数或编译器向量扩展。

  • WebAssembly SIMD 无法控制管理浮点舍入模式或处理非正规数。

  • 缓存行预取指令不可用,对这些函数的调用将编译,但被视为无操作。

  • 非对称内存栅栏操作不可用,但在启用 SharedArrayBuffer (-pthread) 时将实现为完全同步内存栅栏,或者在未启用多线程(默认)时实现为无操作。文章来源地址https://www.toymoban.com/news/detail-677257.html

CG

  • https://blog.csdn.net/qq_32916805/article/details/117637192
  • https://www.uio.no/studier/emner/matnat/ifi/IN3200/v19/teaching-material/avx512.pdf
  • 头文件 immintrin.h : https://stackoverflow.com/questions/11228855/header-files-for-x86-simd-intrinsics
  • https://www.tommesani.com/Docs.html
  • SIMD指令编程demo
  • https://zzqcn.github.io/perf/simd_demo.html
  • 【manim】动画演示SSE指令集SIMD intrinsics(第二期:整数篇)
  • [GSoC 2020] OpenCV.js: WASM SIMD optimization 2.0
  • meshoptimizer: WebAssembly SIMD
  • AssemblyScript A TypeScript-like language for WebAssembly

到了这里,关于webassembly006 SIMD 矢量运算的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 发布 VectorTraits v1.0,它是 C# 下增强SIMD向量运算的类库

    VectorTraits: SIMD Vector type traits methods (SIMD向量类型的特征方法). NuGet: https://www.nuget.org/packages/VectorTraits/1.0.0 源代码: https://github.com/zyl910/VectorTraits 总所周知,使用SIMD指令集,能够加速 多媒体处理(图形、图像、音频、视频...)、人工智能、科学计算 等。 然而,传统的SIMD编程存

    2024年02月09日
    浏览(32)
  • 【ARMv8 SIMD和浮点指令编程】浮点加减乘除指令——四则运算

    浮点指令有专门的加减乘除四则运算指令,比如 FADD、FSUB、FMUL、FDIV 等。 1 FADD (scalar) 浮点加法(标量)。该指令将两个源 SIMDFP 寄存器的浮点值相加,并将结果写入目标 SIMDFP 寄存器。 该指令可以产生浮点异常。根据 FPCR 中的设置,异常会导致在 FPSR 中设置标志,或者生成同

    2024年02月05日
    浏览(43)
  • C# 使用SIMD向量类型加速浮点数组求和运算(5):如何查看Release程序运行时汇编代码

    作者: zyl910 目录 一、引言 二、办法说明 2.1 基本办法 2.2 Release程序如何设置断点 2.3 如何避免“分层编译”的误导 2.4 实际演练(汇编调试) 2.4.1 进入断点 2.4.2 单步调试 2.4.3 观察主循环的汇编代码 三、结语 参考文献 前面的几篇文章里,介绍了 C# 编写向量算法的各种办法

    2024年02月12日
    浏览(26)
  • C# 使用SIMD向量类型加速浮点数组求和运算(4):用引用代替指针, 摆脱unsafe关键字,兼谈Unsafe类的使用

    作者: zyl910 目录 一、引言 二、办法说明 2.1 历史 2.2 局部引用变量与引用所指的值(类似指针的 地址运算符 、间接运算符 * ) 2.3 重新分配局部引用变量(类似指针直接赋值) 2.4 引用地址调整(类似指针加减法) 2.5 引用地址比较(类似指针比较) 2.6 重新解释(类似C++的

    2024年02月15日
    浏览(32)
  • ArcGIS将两个相同范围但不同比例或位置的矢量数据移动到相同位置

    有两个市图层,一个是正确经纬度的市行政范围图层,另一个是其他软件导出获取的不正确经纬度信息或缺失信息。 如果单纯的依靠移动图层,使不正确的移动到正确位置需要很久。尝试定义投影等也不能解决。 使用ArcMap 的空间校正工具条,进行矢量数据的配准。 开始编辑

    2024年02月10日
    浏览(33)
  • C++的移动赋值运算符

    C++的移动赋值运算符是一种特殊的赋值运算符,用于将资源从一个对象转移到另一个对象而不进行深拷贝。移动赋值运算符通常用于支持移动语义,以提高代码的效率和性能。 移动赋值运算符的定义如下: 移动赋值运算符与其他赋值运算符相比,参数类型前面多了一个 ,表

    2024年02月15日
    浏览(54)
  • x86平台SIMD编程入门(1):SIMD基础知识

    SIMD(Single Instruction, Multiple Data)是一种并行计算技术,它通过向量寄存器存储多个数据元素,并使用单条指令同时对这些数据元素进行处理,从而提高了计算效率。SIMD已被广泛应用于需要大量数据并行计算的领域,包括图像处理、视频编码、信号处理、科学计算等。许多现代处

    2024年02月06日
    浏览(26)
  • 精选Axure原型设计模板,RP原型组件库(PC端移动端元件库及Axure函数及运算符说明)

    好的原型组件会大大的提高产品经理的工作效率,小7在陆续整理、精选Axure 8的原型设计模板,包含了原型设计的常用元素和AxureRP 8函数及运算符的说明文档,及各种设备模板框架。 本文也是基于小7另一篇文章的补充, 更多更详细的资料也可以阅读以下链接进行获取 超详细

    2024年02月04日
    浏览(30)
  • C++ remove_if函数(遍历元素,将满足条件的元素移动到容器的末尾)(C++一元函数对象)(括号运算符operator())

    C++ 中的 remove_if 函数是用于从容器中删除满足指定条件的元素的算法。它定义在头文件 algorithm 中,函数签名如下: 其中: first 和 last 分别表示容器中要处理的元素的范围,其中 first 指向第一个要处理的元素,last 指向最后一个要处理的元素的下一个位置。这个范围应该是左

    2024年02月14日
    浏览(26)
  • 编解码-性能优化-SIMD

    SIMD(Single Instruction Multiple Data)是 CPU 硬件层面支持的用于对数据进行 并行 操作。 原理:采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术 它的 指令集存在如下: X86 下的实现为 MMX 、 S

    2024年02月06日
    浏览(19)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包