SIMD指令集介绍

# 介绍

本学期,我们将在多项作业中使用 SIMD(单指令多数据)指令。这些是在称为向量的宽寄存器上运行的指令集。对于我们的作业,这些向量通常为 256 位宽,尽管您可能偶尔使用 128 位版本。通常,作用于这些宽寄存器的指令会将其视为值数组。然后,他们将对数组中的每个值独立执行操作。在硬件中,这可以通过多个并行工作的ALU来实现。因此,尽管这些指令执行的算术比“正常”指令多很多倍,但它们可以与正常指令一样快。

通常,我们将使用“内部函数”访问这些指令。这些函数通常直接对应于特定的汇编指令。这将使我们能够编写始终如一地访问此特殊功能的 C 代码,而不会失去拥有 C 编译器的所有好处。

#  内联参考

我们将使用的内在功能是英特尔定义的接口。因此,英特尔的文档(可在此处找到)是这些功能的综合参考。请注意,本文档包含与实验室计算机上不支持的指令相对应的函数。为避免看到这些,请务必仅选中侧面标有“AVX”、“AVX2”和“SSE”到“SSE4.2”的框。

英特尔的参考资料通常描述了伪代码中的指令,这些指令使用诸如

```
a[63:0] := b[127:64]
```

表示将向量 B 的位 64 到 127(含)分配给向量 A 的位 0 到 63。

#  头文件

若要使用内部函数,需要包含相应的头文件。对于内联函数,我们将使用它是:

```
#include <smmintrin.h>
#include <immintrin.h>
```

# 用 C 语言表示向量

为了表示可能存储在 C 寄存器之一中的 256 位值,我们将使用以下类型之一:

* __m256 (8个float)
* __m256d (4个double)
* __m256i (n个int)

由于其中每个都只是一个 256 位值,因此,如果要使用的函数需要“错误”类型的值,则可以在这些类型之间进行转换。例如,您可能希望使用旨在加载浮点值以加载整数的函数。在内部,期望这些类型的函数只是操作寄存器或内存中的 256 位值。

## 类型和内部函数的 128 位版本

还有 128 位矢量类型和相应的指令。要使用它,在大多数情况下,您可以在类型名称中替换为 _mm256_ 和 _mm_ __m128 在类型名称中替换为 __m256 。

在某些情况下,仅存在 256 位版本的指令。

## 设置和提取值

如果要加载 128 位值的常量,则需要使用内部函数之一。最容易的是,您可以使用名称以 开头 _mm_setr 的函数之一。例如:

```
__m256i values = _mm256_setr_epi32(0x1234, 0x2345, 0x3456, 0x4567, 0x5678, 0x6789, 0x789A, 0x89AB);
```

make 包含 values 8 个 32 位整数, , , 0x3456 , 0x4567 0x1234 0x2345 0x5678 , , , , 0x6789 0x789A . 0x89AB 然后,我们可以通过执行以下操作来提取这些整数中的每一个:

```
int first_value = _mm256_extract_epi32(values, 0);
// first_value == 0x1234
int second_value = _mm256_extract_epi32(values, 1);
// second_value == 0x2345
```

请注意,只能将常量索引传递给 和类似函数的 _mm256_extract_epi32 第二个参数。

## 加载和存储值

要从内存加载值数组或将值数组存储到内存中,我们可以使用以 或 _mm256_storeu 开头 _mm256_loadu 的内联函数:

```
int arrayA[8];
_mm256_storeu_si256((__m128i*) arrayA, values);
// arrayA[0] == 0x1234
// arrayA[1] == 0x2345
// ...

int arrayB[8] = {10, 20, 30, 40, 50, 60, 70, 80};
values = _mm256_loadu_si256((__m128i*) arrayB);
// 10 == arrayB[0] == _mm256_extract_epi32(values, 0)
// 20 == arrayB[1] == _mm256_extract_epi32(values, 1)
// ...
```

##  算术

要实际对值执行算术运算,每个支持的数学运算都有函数。例如:

```
__m256i first_values =  _mm256_setr_epi32(10, 20, 30, 40);
__m256i second_values = _mm256_setr_epi32( 5,  6,  7,  8);
__m256i result_values = _mm256_add_epi32(first_values, second_values);
// _mm_extract_epi32(result_values, 0) == 15
// _mm_extract_epi32(result_values, 1) == 26
// _mm_extract_epi32(result_values, 2) == 37
// _mm_extract_epi32(result_values, 3) == 48
```

## 向量中不同类型的值

这些示例将 256 位值视为 8 个 32 位整数的数组。有一些指令处理许多不同类型的值,包括其他大小的整数或浮点数。您通常可以通过函数名称中指示值类型的存在来判断需要哪种类型。例如,“epi32”表示 an __m256 中的“8 个 32 位值”或 ( __m128 名称代表“扩展打包整数,32 位”)。您将在名称中看到其他一些约定:

* si256 – 有符号 256 位整数
* si128 – 有符号 128 位整数
* epi8 , , epi32 — epi64 有符号 8 位整数(A 中的 32 个和 A __m256 __m128 中的 16 个)或有符号 32 位整数或有符号 64 位整数的向量
* epu8 — 无符号 8 位整数的 vecotr(当操作对有符号和无符号数字的操作之间存在差异时,例如转换为更大的整数或乘法)
* epu16 , epu32 — 无符号 16 位整数或 8 个无符号 32 位整数数组(当操作与有符号不同时)
* ps — “打包单” — 8 个单精度浮子
* pd — “打包双倍” — 4 双倍
* ss — 一个浮点数(仅使用 256 位或 128 位值的 32 位)
* sd — 一个双精度值(仅使用 256 位或 256 位值的 64 位)

#  示例(在 C 中)

以下两个 C 函数是等效的

```
int add_no_AVX(int size, int *first_array, int *second_array) {
    for (int i = 0; i < size; ++i) {
        first_array[i] += second_array[i];
    }
}

int add_AVX(int size, int *first_array, int *second_array) {
    int i = 0;
    for (; i + 8 <= size; i += 8) {
        // load 256-bit chunks of each array
        __m256i first_values = _mm_loadu_si256((__m256i*) &first_array[i]);
        __m256i second_values = _mm_loadu_si256((__m256i*) &second_array[i]);

        // add each pair of 32-bit integers in the 256-bit chunks
        first_values = _mm256_add_epi32(first_values, second_values);
    
        // store 256-bit chunk to first array
        _mm_storeu_si256((__m256i*) &first_array[i], first_values);
    }
    // handle left-over
    for (; i < size; ++i) {
        first_array[i] += second_array[i];
    }
}
```

# 精选的方便的内在函数:

##  算术

* _mm256_add_epi32(a, b) — 将其 __m256i 参数视为 8 个 32 位整数。如果 a 包含 32 位整数 a0, a1, a2, a3, a4, a5, a6, a7 并 b 包含 b0, b1, b2, b3, b4, b5, b6, b7 ,则返回 a0 + b0, a1 + b1, a2 + b2, a3 + b3, a4 + b4, a5 + b5, a6 + a6, a7 + b7 。(与 vpaddd 指令相对应。
* _mm256_add_epi16(a, b) — 与 _mm256_add_epi32 16 位整数相同,但使用 16 位整数。如果 a 包含 16 位整数 a0, a1, ..., a15 并 b 包含 b1, b2, ..., b15 ,则返回 a0 + b0, a1 + b1, ..., a15 + b15 。(与 vpaddw 指令相对应。
* _mm256_add_epi8(a, b) — 与 _mm256_add_epi32 8 位整数相同,但使用 8 位整数。
* _mm256_mullo_epi16(x, y) :将 x 和 y 视为 16 位有符号整数的向量,将每对整数相乘,并将结果截断为 16 位。
* _mm256_mulhi_epi16(x, y) :将 x 和 y 视为 16 位有符号整数的向量,将每对整数相乘得到一个 32 位整数,然后返回每个 32 位整数结果的前 16 位。
* _mm256_srli_epi16(x, N) :处理 x 和 16 位有符号整数的向量,并返回逻辑上将每个右移的结果 N 。(还有 epi32 32 位或 64 位整数的 and epi64 变体。
* _mm256_slli_epi16(x, N) :处理 x 和 16 位有符号整数的向量,并返回将每个向左移动的结果 N 。(还有 epi32 32 位或 64 位整数的 and epi64 变体。
* _mm256_hadd_epi16(a, b) — (“horizontal add”) 将其 __m128i 参数视为 16 位整数的向量。如果 a contains 和 b contains b0, b1, b2, b3, ..., b15 a0, a1, a2, a3, ..., a15 ,则返回 a0 + a1, a2 + a3, a4 + a5, a6 + a7, b0 + b1, b2 + b3, b4 + b5, b6 + b7, a8 + a9, a10 + a11, a12 + a13, a14 + a15, b8 + b9, b10 + b11, b12 + b13, b14 + b15 。请注意,这通常比 _mm_add_epi16 慢得多。(与 vphaddw 指令相对应。

##  加载/存储

* _mm256_loadu_si256 , _mm256_storeu_si256 — 向内存加载或存储 256 位或从内存加载或存储 256 位。请注意,您可以使用 _mm256_storeu_si256 存储到临时数组中,如下所示:

  ```
   unsigned short values_as_array[16];
   __m256i values_as_vector;

   _mm256_storeu_si128((__m256i*) &values_as_array[0], values_as_vector);
  ```
* _mm_loadu_si128 , _mm_storeu_si128 — 将 128 位加载或存储到内存或从内存加载或存储。(对应于 vmovdqu 说明。它们的工作方式与 完全相同, _mm256_loadu_si256 只是它们使用 type __m128i 而不是 __m256i .
* 要存储向量中的 64 位或 32 位,一种方法是使用提取操作和 memcpy:

  ```
   unsigned short first_four_values_as_array[4];
   __m256i values_as_vector;

   *(long*)(&first_four_values_as_array[0]) = _mm256_extract_epi64(values_as_vetor, 0);
  ```

  (此代码实际上不是标准投诉;它违反了“严格别名”规则。但是在 SIMD 分配的 Makefile 中,我们使用 compiler 选项 -fno-strict-aliasing 禁用了它。不违反严格别名规则的替代方法是使用联合,而不是将指针转换为 int* or to use memcpy ,这通常针对小副本进行了优化。
* _mm_cvtsi32_si128 :将 32 位加载到 128 位向量中:

  ```
   unsigned short values[2];
   __m128i values_as_vector; // only using first 32 bits = 2 shorts
   values_as_vector = _mm_cvtsi32_si128( *(int*) &values[0]);
  ```

  (此代码实际上不是标准投诉;它违反了“严格别名”规则。但是在 SIMD 分配的 Makefile 中,我们使用 compiler 选项 -fno-strict-aliasing 禁用了它。不违反严格别名规则的替代方法是使用联合,而不是将指针转换为 int* .)
* _mm_cvtsi32_si128 :将 64 位加载到 128 位向量中:

  ```
   unsigned short values[4];
   __m128i values_as_vector; // only using first 64 bits = 4 shorts
   values_as_vector = _mm_cvtsi64_si128( *(long*) &values[0]);
  ```

  (此代码实际上不是标准投诉;请参阅上面的评论) _mm_cvtsi32_si128
* 要在 256 位向量中加载 32 位或 64 位,可以使用 _mm_cvtsi32_si128 或 _mm_cvtsi32_si256 一起使用 _mm266_zextsi128_si256 将 128 位向量转换为 256 位向量。
* _mm256_maskstore_epi32(int *addr, __m256i mask, __m256i a) — 存储 a at addr 的 32 位值,但仅存储 mask 指定的 32 位值。如果设置了每个 32 位整数 mask 的最高有效位(即符号),则存储值。例如:

  ```
   int values[8] = { 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF };
   __m256i a =    __m256_setr_epi32(1,2,3,4,5,6,7,8);
   __m256i mask = __m256_setr_epi32(0,-1,0,0,-1,0,-1,-1);
   _mm256_maskstore_epi32(&values[0], mask, a);
  ```

  应导致包含以下内容的值

  ```
   { 0xF, 2, 0xF, 0xF, 5, 0xF, 7, 8 }
  ```
* 有关详细信息,请参阅英特尔的参考资料,在“加载”和“存储”类别下

##  设置常量

* _mm256_setr_epi32 — 返回一个 __m256i 包含指定 32 位整数的值。第一个整数参数将位于写入内存时地址最低的部分 __m256i 。例如:

  ```
   __m256i value1 = _mm256_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7);
  ```

  产生 value1 与 in value2 相同的结果

  ```
   int array[8] = {0, 1, 2, 3, 4, 5, 6, 7};
   __m256i value2 = _mm256_loadu_si256((__m256i*) &array[0]);
  ```
* _mm_setr_epi32 — 返回一个 __m128i 包含指定 32 位整数的值。第一个整数参数将位于写入内存时地址最低的部分 __m128i 。例如:

  ```
   __m128i value1 = _mm_setr_epi32(0, 1, 2, 3);
  ```

  产生 value1 与 in value2 相同的结果

  ```
   int array[4] = {0, 1, 2, 3, 4, 5, 6, 7};
   __m128i value2 = _mm_loadu_si128((__m256i*) &array[0]);
  ```
* _mm256_setr_epi16 — 与 _mm256_setr_epi32 16 位整数相同,但使用 16 位整数。例如:

  ```
   __m256i value1 = _mm256_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
  ```

  产生 value1 与 in value2 相同的结果

  ```
   short array[8] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
   __m256i value2 = _mm256_loadu_si256((__m256i*) &array[0]);
  ```
* _mm256_setr_epi8 , — 与 _mm256_setr_epi32 和 _mm_setr_epi32 相同, _mm_setr_epi8 但具有 8 位整数。
* _mm_set1_epi32 , , — 返回一个 __m128i 值,表示适当大小的值数组, _mm_set1_epi16 _mm_set1_epi8 其中数组的每个元素都具有相同的值。例如:

  ```
   __m128i value = _mm_set1_epi16(42);
  ```

  具有与以下相同的效果:

  ```
   __m128i value = _mm_setr_epi16(42, 42, 42, 42,  42, 42, 42, 42);
  ```
* _mm256_set_epi8 , etc. — 与 _mm256_setr_epi8 等相同,但其参数的顺序相反
* 有关更多信息,请参阅英特尔的参考资料,在“设置”类别下

## 提取部分值

* _mm256_extract_epi32(a, index) 从 256 位向量中提取 index 'th 32 位整数 a 。索引为 0 的整数是将存储在最低内存地址的整数,如果 a 复制到内存中。 index 必须是一个常量。
   例如

  ```
   __m256i a = _mm256_setr_epi32(0, 10, 20, 30, 40, 50, 60, 70);
   int x = _mm256_extract_epi32(a, 2);
  ```

  20 分配给 x 。
* _mm_extract_epi32(a, index) 从 128 位向量中提取 index 'th 32 位整数 a 。 index 必须是常量。
* _mm256_extract_epi16(a, index) 与 _mm256_extract_epi32 16 位整数相同,但具有 16 位整数
* _mm256_extracti128_si256(a, index) 从 256 位向量中提取 index 128 位向量 a 。 index 必须是常量。
   例如

  ```
   __m256i a = _mm256_setr_epi32(0, 10, 20, 30, 40, 50, 60, 70);
   __m128i result = _mm256_extracti128_si256(a, 1);
  ```

   相当于

  ```
   __m128i result = _mm_setr_epi32(40, 50, 60, 70);
  ```
* 有关更多信息,请参阅英特尔的参考资料,搜索“提取”或在“Swizzle”和“Cast”类别下查找。

## 在值类型之间转换

* _mm256_cvtepu8_epi16(eight_bit_numbers) :采用包含 16 个 8 位数字的 128 位向量,并将其转换为包含 16 个 16 位有符号整数的 256 位向量。例如:

  ```
   __m128i value1 = _mm_setr_epi8(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150);
   __m256i value2 = _mm256_cvtepu8_epi16(value1);
  ```

  导致 value2 包含与我们执行的相同的值:

  ```
   __m256i value2 = _mm256_setr_epi16(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150);
  ```
* []()_mm256_packus_epi16(a, b) 获取 256 位向量中的 16 位有符号整数 a , b 并将它们转换为 8 位无符号整数的 256 位向量。结果包含 的前 a 8 个整数,后跟 的前 8 个整数,后跟 的最后 8 个整数 b a ,后跟 的最后 8 个整数 b 。超出范围的值设置为 255 或 0。
   例如:

  ```
   __m256i a = _mm256_setr_epi16(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160);
   __m256i b = _mm256_setr_epi16(170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 25, 15, 5, -5, -15);

   __m256i result = _mm256_packus_epi16(a, b)
  ```

  设置 result 与我们所做的相同:

  ```
   __m256i result = _mm256_setr_epu8(
       10, 20, 30, 40, 50, 60, 70, 80, /* first 8 integers from a */
       170, 180, 190, 200, 210, 220, 230, 240, /* first eight integers from b */
       90, 100, 110, 120, 130, 140, 150, /* last 8 integers from a */
       250, 255, 255, 25, 15, 5, 0, 0,  /* last 8 integers from b */
           /* 260, 270 became 255;  -5, -15 became 0 */
   );
  ```
* _mm256_zextsi128_si256(a) 采用 128 位向量 a ,并通过添加 0 将其转换为 256 位向量。
* 有关更多信息,请参阅英特尔在“Swizzle”和“Move”和“Cast”类别下的参考。

## 重新排列 256 位值

* _mm256_permute2x128_si256(a, b, mask) 采用两个 256 位向量, a 并 b 根据 mask 将这些向量的 128 位半部分组合成一个新的 256 位向量。 mask 是一个单字节整数常量。最低有效半字节指定放置在结果向量的最低地址中的值,最高有效半字节指定放置在结果向量的最高地址中的值。
  每个掩码半字节选择的值为:

  * 0 选择前 128 位 a
  * 1 选择第二个 128 位 a
  * 2 选择前 128 位 b
  * 3 选择第二个 128 位 b
  * 4 到 15 选择常量 0 (忽略 a 和 b 的值)

  例如,要重复 a 的第二个 128 位,可以提供如下示例所示 0x11 的掩码:

  ```
   __m256i a = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
   __m256i b = _mm256_setr_epi32(8, 9, 10, 11, 12, 13, 14, 15);
   __m256i result = _mm256_permute2x128_si256(a, b, 0x11);
   // result == _mm256_setr_epi32(4, 5, 6, 7, 4, 5, 6, 7)
  ```

  要生成前 128 位后跟后跟 1 位后跟 128 位 a b 的结果,将提供如下 0x30 掩码:

  ```
   __m256i a = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
   __m256i b = _mm256_setr_epi32(8, 9, 10, 11, 12, 13, 14, 15);
   __m256i result = _mm256_permute2x128_si256(a, b, 0x30);
   // result == _mm256_setr_epi32(0, 1, 2, 3, 12, 13, 14, 15)
  ```
* _mm256_unpackhi_epi16(a, b) 将 16 位整数与 256 位向量中每个 128 位半部分的上四分之一交错, a 然后 b .例如:

  ```
   __m256i a = _mm256_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
   __m256i b = _mm256_setr_epi16(16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31);
   __m256i result = _mm256_unpackhi_epi16(a, b);
  ```

  与

  ```
   __m256i result = _mm256_setr_epi16(
       /* top quarter of first half of a and b */
       4, 20, 5, 21, 6, 22, 7, 23,
       /* top quarter of second half of a and b */
       12, 28, 13, 29, 14, 30, 15, 31
   )
  ```
* _mm256_unpacklo_epi16(a, b) 就像, _mm256_unpackhi_epi16 但它从 和 的每半部分 a b 的底部四分之一取 16 位整数
* _mm256_permutevar8x32_epi32(x, indexes) — 通过为向量中的每个 32 位索引生成一个 32 位值的向量 indexes ,从向量中检索该索引处的 32 位值 x 并将其放入结果中。例如:

  ```
   __m256i x = _mm256_setr_epi32(10, 20, 30, 40, 50, 60, 70, 80)
   __m256i indexes = _mm256_setr_epi32(3, 3, 0, 1, 2, 3, 6, 7);
   __m256i result = _mm256_permutevar8x32_epi32(x, indexes)
  ```

  等同于:

  ```
   __m256i reuslt = _mm256_setr_epi32(40, 40, 10, 20, 30, 70, 80);
  ```
* 有关更多信息,请参阅英特尔在“Swizzle”和“Move”以及“Cast”和“Shift”类别下的参考。

## 重新排列 128 位值

* _mm_unpackhi_epi16(a, b) 将 128 位向量上半部分的 16 位整数交错, a 然后 b .例如:

  ```
   __m128i a = _mm_setr_epi16(0, 1, 2, 3, 4, 5, 6, 7);
   __m128i b = _mm_setr_epi16(8, 9, 10, 11, 12, 13, 14, 15);
   __m256i result = _mm_unpackhi_epi16(a, b);
  ```

  与

  ```
   __m128i result = _mm_setr_epi16(
       4, 20, 5, 21, 6, 22, 7, 23,
   )
  ```
* _mm_shuffle_epi8(a, mask) 重新排列 a 根据 的 mask 字节并返回结果。 mask 是 8 位整数 (type __m128i ) 的向量,指示如何重新排列每个字节:

  * 如果掩码中的字节设置了高位(大于 127),则输出的相应字节为 0;
  * 否则,输入中指定的字节号将复制到输出的相应字节。字节使用 0 进行编号,以表示如果将向量复制到内存中,将存储在最低地址中的字节。

   例如:

  ```
   __m128i value1 = _mm_setr_epi8(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160);
   __m128i mask = _mm_setr_epi8(0x80, 0x80, 0x80, 5, 4, 3, 0x80, 7, 6, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80);
   __m128i value2 = _mm_shuffle_epi8(value1, mask);
  ```

  应产生与以下结果相同的结果:

  ```
   __m128i value2 = _mm_setr_epi8(0, 0, 0, 60, 50, 40, 0, 80, 70, 0, 0, 0, 0, 0, 0, 0, 0);
       /* e.g. since 3rd element of mask is 5, 3rd element of output is 60, element 5 of the input */
  ```
* 有关更多信息,请参阅英特尔在“Swizzle”和“Move”以及“Cast”和“Shift”类别下的参考。

# 示例(组装指令)

 指令

```
      paddd %xmm0, %xmm1

```

接收两个 128 位值,一个在寄存器中,另一个在寄存器 %xmm0 %xmm1 中。这些寄存器中的每一个都被视为两个 64 位值的数组。将每对 64 位值相加,并将结果存储在 %xmm1 中。

例如,如果 %xmm0 包含 128 位值(以十六进制写入):

```
0x0000 0000 0000 0001 FFFF FFFF FFFF FFFF 
```

并 %xmm1 包含 128 位值(以十六进制写入):

```
0xFFFF FFFF FFFF FFFE 0000 0000 0000 0003 
```

然后 %xmm0 ,将被视为包含数字和 (或 0xFFFFFFFFFFFFFFFF ),并 %xmm1 被视为包含数字 -2 1 和 -1 3 。 paddd 将添加 1 和 -2 to produce -1 and -1 and 3 to produce 2, so the final value of %xmm1' 将是:

```
0xFFFF FFFF FFFF FFFF 0000 0000 0000 0002
```

如果我们将此值解释为两个 64 位整数的数组,则为 -1 和 2 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值