39、高性能计算与消息传递接口系统详解

高性能计算与消息传递接口系统详解

在当今的计算领域,高性能计算和并行处理变得越来越重要。本文将深入探讨高性能计算中的计时方法,以及消息传递接口(MPI)系统的安装、使用和应用。

高性能计算中的计时方法

在进行程序性能评估时,准确的计时至关重要。为了获取准确的计时结果,程序需要运行较长时间。一种常见的方法是多次执行相同的计算,例如在边缘检测算法计时时,重复执行该算法一百次,然后将总时间除以 100 以得到准确的单次执行时间。

需要注意的是,不同试验中的计时结果可能不会完全一致。因为 PC 在执行被计时程序的同时还会进行其他操作,操作系统会干扰计时过程,例如可能会将程序换出以执行其他任务,或者 Windows 可能会进行磁盘温度校准或轮询设备,这些都会增加程序执行的测量时间。为了获得更准确的计时结果,可以多次重复测量并取平均值。

使用 QueryPerformanceCounter

Microsoft Windows 操作系统(至少包括 7 和 XP)提供了一个系统特定的高分辨率计时器 QueryPerformanceCounter 。它的精度可达纳秒级别,并且使用 64 位表示时间。虽然使用起来有些复杂,但它与简单的 clock 函数类似,使用方式也基本相同。

QueryPerformanceCounter 接受一个参数,该参数是指向 LARGE_INTEGER 的指针。 LARGE_INTEGER 实际上是一个表示 64 位数字的结构,用于高分辨率时间,并且可以在不同系统间移植(但可能仅在 Windows 上使用)。在计时时,需要在要计时的代码前后分别调用 QueryPerformanceCounter ,并将用于存储时间的变量作为参数传递。

LARGE_INTEGER 结构的数字部分称为 QuadPart 。如果 stop start 分别是用于存储代码执行开始和结束时间的 LARGE_INTEGER 变量,那么代码执行的持续时间为 stop.QuadPart - start.QuadPart ,并且可以将其转换为 double 类型而不会损失太多精度。以下是使用 QueryPerformanceCounter 进行计时的 C 代码示例:

/* Timing C code using QueryPerformanceCounter */
#include "mpi.h"
#include <stdio.h>
#include <windows.h>
#include <math.h>
#include <time.h>

#define BUFSIZE 1024
#define ITERS 1000

int main(int argc, char *argv[])
{
    int i, j, niter = 1000;
    LARGE_INTEGER start, stop, quantum;
    double xtime, dat[BUFSIZE];
    for (niter = ITERS; niter < ITERS * 1000; niter *= 2)
    {
        QueryPerformanceCounter(&start);
        for (j = 0; j < niter; j++)
        {
            for (i = 0; i < BUFSIZE; i++)
            {
                dat[i] = sin(3.1415926535 * i / 1024.0);
            }
        }
        QueryPerformanceCounter(&stop);
        QueryPerformanceFrequency(&quantum);
        xtime = (double)(stop.QuadPart - start.QuadPart) / (double)(quantum.QuadPart);
        printf("Done. Time = %lf Iterations %d per iteration %lf\n", xtime, niter, xtime / niter * 1000);
    }
    printf("Returning.\n");
    return 0;
}

使用该代码时需要注意两点:一是需要包含头文件 windows.h ;二是计时器的分辨率由 QueryPerformanceFrequency 函数返回,将时间差除以该值即可得到以秒为单位的时间。该程序的输出结果如下:

Iterations Time Per iteration
1000 0.045933 0.045933
2000 0.091912 0.045956
4000 0.184248 0.046062
8000 0.367418 0.045927
16000 0.736176 0.046011
32000 1.527363 0.047730
64000 2.996671 0.046823
128000 5.883134 0.045962
256000 11.779256 0.046013
512000 23.556424 0.046009

可以发现,这个高分辨率计时器在 PC 上产生的结果与简单的 clock 函数基本相同。对于 16000 次迭代及以上,两种计时方法的结果在小数点后第四位有所不同,由于时间单位是毫秒,所以差异为微秒。在最坏的情况下,它们在小数点后第三位不同,这已经相当不错了。

消息传递接口(MPI)系统

消息传递接口(MPI)系统是一个经过充分测试且免费可用的系统,用于在 PC 上实现消息传递。MPI 的基本原理是向网络中的计算机发送消息或信息包,每个消息可以包含数据和计算机要处理的命令,计算机处理后会返回包含结果的消息。通常,中央计算机会将数据分配给卫星计算机,并将返回的数据包排序到正确的位置,这意味着可能会存在瓶颈。MPI 是一种标准的消息传递协议,实现良好,易于安装和使用。

安装 MPI

MPI 需要在实现图像处理操作的源代码中添加特殊调用,因此需要将其安装为与方便的编译器一起使用。以下是安装 MPI 的具体步骤:
1. 从 www.mcs.anl.gov/research/projects/mpich2/ 下载 MPI 可执行文件。
2. 运行安装脚本 mpich2 - 1.2.1p1 - win - ia32.msi 。MPI 通常会安装到 C:\Program Files\MPICH2 目录,相关文档可以在 www.mcs.anl.gov/research/projects/mpich2/documentation/index.php?s=docs 在线查看。
3. 设置编译器(以 Microsoft Visual C++ 2008 Express Edition 为例):
- 在 Project ➪ Properties ➪ Linker ➪ Input 中,将 mpi.lib 添加到 Additional Dependencies
- 在 Project ➪ Properties ➪ C/C++ ➪ General 中,将 C:\Program Files\MPICH2\include 添加到 Additional Include Directories 。该路径可能会根据 MPI 的安装方式而改变,一般使用 X\include ,其中 X 是 MPI 安装目录的完整路径。
- 在 Project ➪ Properties ➪ Linker ➪ General 中,将 C:\Program Files\MPICH2\lib 添加到 Additional Library Directories 。同样,该路径可能会改变,一般使用 X\library ,其中 X 是 MPI 安装目录的完整路径。
4. 运行 wmpiregister ,并输入用于 PC 的用户名和密码。
5. 运行 wmpiconfig 并选择配置选项。

完成以上步骤后,MPI 就可以在一台计算机上设置好了。

使用 MPI

以下是使用 MPI 的基本步骤和示例代码:
1. 初始化 MPI 系统 :编译用于 MPI 的程序必须包含 mpi.h ,其中包含相关类型和常量的定义。主程序在执行其他操作之前必须初始化 MPI 系统,通过调用 MPI_Init(0, 0) 来完成。
2. 获取进程信息 :当程序启动时,需要指定要使用的处理器数量(假设为 N),可以在命令行或 WMPIEXEC 窗口的框中指定。调用 MPI_Init 会创建 N - 1 个进程,创建进程的第一个进程编号为 0,其他进程编号从 1 到 N - 1 依次递增。可以通过调用 MPI_Comm_rank(MPI_COMM_WORLD, &my_rank) 来获取当前进程的编号(即 rank ),该编号是一个介于 0 到 N - 1 之间的值,值为 0 表示这是根进程或主进程,其他值是标识进程的唯一整数。通过调用 MPI_Comm_size(MPI_COMM_WORLD, &size) 可以获取创建的进程总数,返回的值为 N - 1。
3. 实现并行程序 :一种常见的使用 MPI 实现并行程序的方法是让主进程(即编号为 0 的进程)读取或获取数据,并将其分发给其他进程(称为从进程),从进程计算结果后将数据返回给主进程,主进程将结果整理并保存或打印。以下是示例代码:

if (my_rank == 0)
    // 主进程
    master(size);
else
    // 从进程
    slave(my_rank, size);

这种方式将程序逻辑清晰地分为两部分,大多数基于 MPI 的程序都可以这样编写。
4. 终止 MPI 系统 :最后,需要终止 MPI 系统以释放系统资源并销毁进程,通过调用 MPI_Finalize() 来完成。

进程间通信

进程间通信是任何消息传递系统的核心,其复杂性足以撰写多本专著。在消息传递中,最重要的是发送和接收消息的能力,消息简单来说就是带有头部的数据集合。虽然这个概念看似简单,但不正确的消息发送和接收是程序员在使用 MPI 时遇到问题的主要原因。

  • 发送消息 :可以使用 MPI_Send 函数发送消息,其函数原型如下:
MPI_Send (data, count, data_type, destination, tag, communicator)
- `data`:指向要发送信息的指针,其类型为 `data_type`。
- `count`:要发送的该类型元素的数量。
- `destination`:表示要接收数据的进程编号的整数。
- `tag`:一个整数,可以表示用户希望的任何含义,通常用于指示消息类型,例如 1 可以表示图像数据,2 可以表示算法的参数等。发送的消息类型通常应与接收方期望的类型同步。
- `communicator`:表示通信细节的句柄,通常使用标准的 `MPI_COMM_WORLD` 句柄。

MPI_Send 是一个阻塞发送函数,这意味着调用发送的进程将等待(在操作系统术语中称为阻塞),直到消息被接收后才会继续执行。如果消息永远无法被接收,进程将永远阻塞,即所谓的挂起。如果足够多的进程挂起,或者主程序挂起,那么程序将永远无法终止。因此,避免这些死锁情况对于并行程序的正确编码至关重要。

例如,主进程将图像的一部分发送给从进程(例如进程 2),图像数据复制到名为 data 的数组中,该数组包含灰度像素,数据类型为 unsigned char 。如果要发送图像的三行,每行有 NC 个像素,那么调用如下:

MPI_Send (data, NC*3, MPI_UNSIGNED_CHAR, 2, 1, MPI_COMM_WORLD)

C 或 C++ 定义的每种数据类型都有一个对应的常量,用于向 MPI_Send 指示该类型,这里需要的是 MPI_UNSIGNED_CHAR ,其他选择也很明显,如 MPI_INT MPI_FLOAT 等。 tag 值除非程序员赋予其重要意义,否则系统在大多数情况下会忽略它,这里 tag 值为 1 表示“数据”。

  • 接收消息 :在发送操作的另一端是接收操作,进程通过 MPI_Recv 函数接受来自其他进程的信息,其函数原型如下:
MPI_Recv(data, size, MPI_UNSIGNED_CHAR, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &stat)
- 前三个参数比较明确。
- 第四个参数 `0` 表示该调用期望从进程 0(即主进程)接收消息,也可以从任何进程接收消息,甚至可以不指定发送方。
- `MPI_ANY_TAG` 表示接收操作可以接受任何发送的 `tag`,这是 `tag` 唯一重要的地方。如果指定了特定的 `tag`,那么发送消息的 `tag` 应该与之匹配。
- 最后一个参数是返回的状态,从该状态中可以获取发送消息的大小、实际发送的 `tag`、错误代码等信息。状态代码是一个包含以下字段的结构:
    - `MPI_SOURCE`:发送方的进程编号。
    - `MPI_TAG`:发送的 `tag`。
    - `MPI_ERROR`:与消息关联的任何错误代码。

例如,与上述发送操作对应的接收操作如下:

MPI_Recv (data, NC*3, MPI_UNSIGNED_CHAR, 0, 1, MPI_COMM_WORLD, &status)

如果要接收的数据大小未知,可以指定一个预期的最大大小,这样系统可以分配足够的缓冲区空间。

运行 MPI 程序

网站上的 mpiTest1.c 程序是对 MPI 系统的简单测试,它模拟了并行执行的任意工作负载,可以演示 send recv 调用的使用方式、程序的执行方式,以及并行代码在实际时间上比常规代码更快。

编译并设置好编译器后,该程序应编译为 mpiTest1.exe ,也可以在网站上找到该可执行文件的副本。可以通过以下两种方式运行该程序:
- 使用 wmpiexec :在 Windows 系统下建议使用这种方式。打开 wmpiexec 窗口,在 Application 字段中输入可执行文件的名称(也可以使用浏览按钮)。在示例中, Save Job 按钮上方窗口右上角的数字为 2,表示要创建的进程数量。按下 Execute 按钮后,程序将以两个进程开始运行,输出将显示在下方的大白色区域中。例如,输出行可能显示 Done. Time = 2.205164
- 从命令行运行 :打开 Windows 命令提示符,切换到 mpiTest1.exe 文件所在的目录,然后输入以下命令:

"C:\Program Files\MPICH2\bin\mpiexec.exe" -n 2 -noprompt mpiTest1.exe

该命令假设 MPI 系统已安装在正常位置( C:\Program Files\MPICH2 ), -n 2 参数指定要创建两个进程。在命令末尾添加 > out.txt 可以将程序的输出保存到名为 out.txt 的文件中,而不是显示在屏幕上,有时后续可能需要这些输出。

该程序虽然不做任何有用的工作,但由于它执行计算来代替实际工作,因此需要花费大量时间执行。它是一个虚拟负载,允许按照前面介绍的方式对代码进行计时。

在一台四核 Intel Q8200 计算机上运行该程序,每个处理器的频率为 2.33 GHz,这意味着可以同时执行四个进程:一个主进程(编号为 0)和三个从进程。之后,任何额外的进程将在四个处理器之一上运行,同时还会运行其他进程,因此几乎不会有速度提升。通过反复运行相同的程序,指定 2、3、4、5 和 6 个进程,并使用主进程中的 QueryPerformanceCounter 测量执行时间,得到以下结果:

处理器数量 时间
2 2.02077
3 1.06104
4 0.775137
5 0.622573
6 0.741093

可以看到,在使用 5 个处理器之前,时间逐渐减少,这实际证明了系统正在利用并行性来计算结果。在添加第 5 个处理器后速度仍有一点提升,这意味着其中一个处理器除了基本计算外还有一些额外的空闲时间。

实际图像计算

mpiTest1 程序只是一个虚拟程序,它消耗 CPU 时间但不做任何有用的工作。下面我们将构建一个有实际作用的并行程序,考虑使用中值滤波器。

中值滤波器旨在通过用图像中每个像素周围邻域内像素的中值替换该像素来平滑图像中的噪声。例如,一个 13 点中值滤波器会取目标像素最近的 13 个像素,计算中值,并使用该值替换目标像素的值。由于找到中值需要对 13 个像素进行排序并选择排序后列表中的第 7 个作为中值,因此计算量很大。每个这样的滤波器都涉及 Rows x Columns 次对 13 个数字的排序。

主进程向每个从进程发送两条消息。第一条消息包含要发送给从进程的图像大小,即行数和列数,该大小会根据进程数量而变化。如果只有一个从进程,将发送整个图像;如果有两个从进程,则每个从进程将接收一半的图像,依此类推。第二条消息包含像素数据,将第一行的指针作为缓冲区指针传递,指定的行数作为计数。以下是相关代码:

nr = im->info->nr; nc = im->info->nc;
if (size - 1 > 1)
    n = (nr + (size - 1) * 4) / (size - 1);
// 每个处理器的行数
else
    n = nr;
j = 0;
for (partner = 1; partner < size; partner++)
{
    rstart = j;
    rend = j + n + 2;
    if (rend >= nr)
        rend = nr - 1;
    b1[0] = (rend - rstart + 1);
    MPI_Send(b1, 2, MPI_INT, partner, 1, MPI_COMM_WORLD);
    MPI_Send(im->data[rstart], (rend - rstart + 1) * nc, MPI_UNSIGNED_CHAR, partner, 1, MPI_COMM_WORLD);
    j = rend - 3;
}

从进程(在代码中称为 partner )接收到消息后,会计算五次中值滤波器。它们必须按顺序接收来自主进程的两条消息:

MPI_Recv(p, 2, MPI_INT, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &stat);
im1 = newimage(p[0], p[1]);
// p[0] 是行数,p[1] 是列数
im2 = newimage(p[0], p[1]);
MPI_Recv(im1->data[0], (4 + p[0]) * p[1], MPI_UNSIGNED_CHAR, 0, MPI_ANY_TAG, MPI_COMM_WORLD, &stat);

计算完成后,从进程将数据返回给主进程:

MPI_Send(im2->data[0], p[0] * p[1], MPI_UNSIGNED_CHAR, 0, 1, MPI_COMM_WORLD);

主进程接收来自从进程的数据,并将其直接移动到图像数组中:

j = 0;
for (partner = 1; partner < size; partner++)
{
    rstart = j;
    rend = j + n + 2;
    if (rend >= nr)
        rend = nr - 1;
    MPI_Recv(im->data[rstart], (rend - rstart + 1) * nc, MPI_UNSIGNED_CHAR, partner, 1, MPI_COMM_WORLD, &stat);
    j = rend - 3;
    sprintf(name, "H:\\AIPCV\\stage%1d.pgm", partner);
    Output_PBM(im, name);
}

主进程会将每个从进程的工作结果保存为图像文件。对于四处理器运行的结果显示,添加第四个处理器并没有帮助,但使用三个 CPU 可以实现 25%的加速。计时操作进行了四次并取平均值,这是标准做法,因为 N 次测量的平均值比任何一次测量都更能准确估计实际值。具体的计时数据可以参考相关表格。

综上所述,通过使用高分辨率计时器和 MPI 系统,可以实现准确的程序计时和高效的并行计算,特别是在图像处理等领域具有重要的应用价值。在实际使用中,需要注意避免死锁等问题,以确保并行程序的正确执行。同时,根据具体的计算任务和硬件资源,合理选择进程数量可以获得更好的性能提升。

高性能计算与消息传递接口系统详解(续)

总结与性能分析

在前面的内容中,我们详细介绍了高性能计算中精确计时的方法以及消息传递接口(MPI)系统的安装、使用、进程间通信、程序运行和实际图像计算等方面。下面我们对这些内容进行总结,并进一步分析其性能。

计时方法总结
  • 为了获得准确的计时结果,程序需要运行较长时间,可多次执行相同计算并取平均值,以减少操作系统干扰带来的误差。
  • QueryPerformanceCounter 是 Windows 系统提供的高分辨率计时器,精度可达纳秒级别。使用时需包含 windows.h 头文件,通过 QueryPerformanceFrequency 函数获取计时器分辨率,将时间差除以该分辨率得到以秒为单位的时间。其计时结果与简单的 clock 函数基本相同,在 16000 次迭代及以上时,差值在微秒级别。
MPI 系统性能分析
  • 安装与使用 :MPI 系统易于安装和使用,只需按照特定步骤下载、安装并配置编译器即可。使用时,通过 MPI_Init 初始化系统, MPI_Comm_rank MPI_Comm_size 获取进程信息,根据进程编号区分主进程和从进程,最后使用 MPI_Finalize 终止系统。
  • 进程间通信 :进程间通信是 MPI 系统的核心,使用 MPI_Send MPI_Recv 函数进行消息的发送和接收。发送消息时需注意数据类型、目标进程编号和消息标签等参数,接收消息时要确保消息类型匹配,避免死锁情况的发生。
  • 运行性能 :通过 mpiTest1.c 程序的测试,我们发现随着处理器数量的增加,程序执行时间逐渐减少,直到使用 5 个处理器时达到较好的性能。这表明系统能够利用并行性提高计算效率,但当处理器数量超过硬件核心数量时,性能提升有限。
实际图像计算性能

在实际图像计算中,使用中值滤波器进行并行计算。主进程将图像数据分发给从进程,从进程进行计算后将结果返回给主进程。通过多次计时取平均值,我们发现使用三个 CPU 可以实现 25%的加速,但添加第四个处理器并没有带来明显的性能提升。这说明在实际应用中,需要根据具体的计算任务和硬件资源合理选择进程数量,以获得最佳的性能。

优化建议

为了进一步提高高性能计算和 MPI 系统的性能,我们可以考虑以下优化建议:

计时优化
  • 增加迭代次数 :在进行计时时,适当增加迭代次数可以提高计时的准确性,减少操作系统干扰的影响。
  • 多次测量取平均 :多次重复测量并取平均值,能够更准确地估计程序的实际执行时间。
MPI 系统优化
  • 减少通信开销 :在进程间通信时,尽量减少不必要的消息发送和接收,避免频繁的同步操作,以降低通信开销。
  • 负载均衡 :合理分配计算任务,确保各个进程的负载均衡,避免出现某些进程空闲而其他进程忙碌的情况。
  • 异步通信 :使用异步通信函数(如 MPI_Isend MPI_Irecv )代替阻塞通信函数,允许进程在发送或接收消息的同时继续执行其他任务,提高并行效率。
实际图像计算优化
  • 算法优化 :对于中值滤波器等计算密集型算法,可以采用更高效的算法实现,减少排序操作的时间复杂度。
  • 数据局部性 :在数据分配和处理时,尽量提高数据的局部性,减少数据的移动和复制,提高缓存命中率。
未来展望

随着计算机技术的不断发展,高性能计算和并行处理将在更多领域得到广泛应用。未来,我们可以期待以下方面的发展:

硬件技术进步
  • 多核处理器 :随着多核处理器技术的不断发展,硬件核心数量将不断增加,为并行计算提供更强大的支持。
  • 异构计算 :结合 CPU、GPU 等不同类型的计算设备,实现异构计算,进一步提高计算性能。
软件技术发展
  • 新型并行编程模型 :不断涌现的新型并行编程模型将使并行计算更加容易实现和管理。
  • 智能优化算法 :利用人工智能和机器学习技术,实现自动的性能优化和负载均衡。
应用领域拓展
  • 科学计算 :在气象预报、生物信息学、天体物理等领域,高性能计算将发挥更加重要的作用。
  • 工业应用 :在汽车制造、航空航天、电子设计等行业,并行计算将提高产品研发效率和质量。
总结

高性能计算和消息传递接口(MPI)系统为我们提供了强大的计算能力和并行处理手段。通过精确的计时方法和合理的 MPI 系统配置,我们可以实现高效的并行计算,特别是在图像处理等领域具有重要的应用价值。在实际使用中,我们需要注意避免死锁等问题,根据具体的计算任务和硬件资源合理选择进程数量,并不断探索优化方法,以提高系统的性能。随着硬件和软件技术的不断发展,高性能计算和并行处理将在更多领域得到广泛应用,为推动科学技术的进步和社会发展做出更大的贡献。

以下是一个简单的 mermaid 流程图,展示了 MPI 程序的基本执行流程:

graph TD;
    A[开始] --> B[初始化 MPI 系统];
    B --> C[获取进程信息];
    C --> D{判断进程类型};
    D -- 主进程 --> E[读取数据并分发];
    D -- 从进程 --> F[接收数据并计算];
    E --> G[等待从进程结果];
    F --> H[发送计算结果];
    G --> I[整理结果并保存];
    H --> G;
    I --> J[终止 MPI 系统];
    F --> J;
    J --> K[结束];

通过这个流程图,我们可以更清晰地了解 MPI 程序的执行过程,包括主进程和从进程的不同操作以及它们之间的交互。

总之,高性能计算和 MPI 系统是现代计算领域中不可或缺的重要组成部分,掌握其原理和使用方法对于提高计算效率和解决复杂问题具有重要意义。希望本文能够为读者在高性能计算和并行处理方面提供有益的参考和帮助。

【四旋翼无人机】具备螺旋桨倾斜机构的全驱动四旋翼无人机:建模控制研究(Matlab代码、Simulink仿真实现)内容概要:本文围绕具备螺旋桨倾斜机构的全驱动四旋翼无人机展开研究,重点探讨其系统建模控制策略,结合Matlab代码Simulink仿真实现。文章详细分析了无人机的动力学模型,特别是引入螺旋桨倾斜机构后带来的全驱动特性,使其在姿态位置控制上具备更强的机动性自由度。研究涵盖了非线性系统建模、控制器设计(如PID、MPC、非线性控制等)、仿真验证及动态响应分析,旨在提升无人机在复杂环境下的稳定性和控制精度。同时,文中提供的Matlab/Simulink资源便于读者复现实验并进一步优化控制算法。; 适合人群:具备一定控制理论基础和Matlab/Simulink仿真经验的研究生、科研人员及无人机控制系统开发工程师,尤其适合从事飞行器建模先进控制算法研究的专业人员。; 使用场景及目标:①用于全驱动四旋翼无人机的动力学建模仿真平台搭建;②研究先进控制算法(如模型预测控制、非线性控制)在无人机系统中的应用;③支持科研论文复现、课程设计或毕业课题开发,推动无人机高机动控制技术的研究进展。; 阅读建议:建议读者结合文档提供的Matlab代码Simulink模型,逐步实现建模控制算法,重点关注坐标系定义、力矩分配逻辑及控制闭环的设计细节,同时可通过修改参数和添加扰动来验证系统的鲁棒性适应性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值