C++编程:利用ARM硬件加速CRC32计算

news2024/10/11 23:22:30

文章目录

    • 引言
    • 软件实现CRC32
      • 代码解析
    • ARM硬件加速CRC32
      • 代码解析
    • 性能压测
      • 完整的`crc32_benchmark.cpp`实现
      • 代码说明
      • 编译与运行
      • 运行结果示例
      • 结果分析
      • 性能提升原因

引言

本文将介绍如何在ARM架构上通过硬件加速实现高性能的CRC32计算,并与传统的软件实现进行性能对比。

软件实现CRC32

传统的软件实现通常采用逐字节、逐位计算的方法,不使用查找表。这种方法虽然实现简单,但在处理大规模数据时效率较低。以下是基于逐位计算的CRC32实现示例:

#include <cstdint>

// 软件CRC32实现(逐字节、逐位计算,不使用查找表)
class SoftwareCRC32 {
public:
    static constexpr uint32_t CRC32_POLYNOMIAL = 0xEDB88320L;

    // 计算单个字节的CRC32值
    uint32_t calcCRC32Value(int32_t f_data_r) const {
        uint32_t ulCRC = f_data_r & 0xFF;
        for (int i = 0; i < 8; ++i) {
            if (ulCRC & 1)
                ulCRC = (ulCRC >> 1) ^ CRC32_POLYNOMIAL;
            else
                ulCRC >>= 1;
        }
        return ulCRC;
    }

    // 计算整个数据块的CRC32
    uint32_t compute(const uint8_t* data, size_t len, uint32_t crc = 0) const {
        // 初始化CRC32为0xFFFFFFFF
        crc = crc32_start();

        for (size_t i = 0; i < len; ++i) {
            uint8_t byte = data[i];
            uint32_t ulCrcDark = (crc >> 8) & 0x00FFFFFFL;
            uint32_t ulCrcWhite = calcCRC32Value(static_cast<int32_t>((crc ^ byte) & 0xFF));
            crc = ulCrcDark ^ ulCrcWhite;
        }

        // 最终CRC32为crc ^ 0xFFFFFFFF
        crc = crc32_end(crc);
        return crc;
    }

    // 初始化CRC32
    inline uint32_t crc32_start(void) const {
        return 0xFFFFFFFFU; // 标准CRC32初始化值
    }

    // 结束CRC32
    inline uint32_t crc32_end(uint32_t crc) const {
        return crc ^ 0xFFFFFFFFU; // 标准CRC32最终异或
    }
};

代码解析

  1. calcCRC32Value:计算单个字节的CRC32值,逐位处理,不使用查找表。
  2. compute:计算整个数据块的CRC32。流程如下:
    • 初始化CRC32为0xFFFFFFFF
    • 对每个字节执行CRC32计算:
      • 计算ulCrcDark:将当前CRC右移8位,并清除高24位。
      • 计算ulCrcWhite:将当前CRC的低8位与字节进行XOR操作,然后调用calcCRC32Value
      • 更新CRC:ulCrcDark ^ ulCrcWhite
    • 最终CRC32值为crc ^ 0xFFFFFFFF

ARM硬件加速CRC32

现代ARM处理器(特别是ARMv8.1-A及以上版本)提供了专用的CRC32指令,可以显著提升CRC32计算的性能。这些指令包括:

  • __crc32b:处理单字节数据。
  • __crc32h:处理双字节数据(16位)。
  • __crc32w:处理四字节数据(32位)。
  • __crc32d:处理八字节数据(64位)。

利用这些指令,我们可以实现一个高效的硬件加速CRC32计算类。

#include <cstdint>
#include <cstring>
#include <arm_acle.h> // ARM内置函数头文件

// 硬件加速CRC32实现
class HardwareCRC32 {
public:
    // 初始化CRC32
    inline uint32_t crc32_start(void) const {
        return 0xFFFFFFFFU; // 标准CRC32初始化值
    }

    // 结束CRC32
    inline uint32_t crc32_end(uint32_t crc) const {
        return crc ^ 0xFFFFFFFFU; // 标准CRC32最终异或
    }

    // 使用ARM内置函数进行硬件加速的CRC32计算
    inline uint32_t crc32_do(const void *const in_buf, uint32_t crc, const uint64_t in_buf_len) const {
        const uint8_t* data = static_cast<const uint8_t*>(in_buf);
        const uint8_t* end = data + in_buf_len;

        // 处理8字节数据
        while (data + 8 <= end) {
            uint64_t val;
            memcpy(&val, data, sizeof(uint64_t)); // 确保安全读取
            crc = __crc32d(crc, val); // 使用__crc32d
            data += 8;
        }

        // 处理4字节数据
        while (data + 4 <= end) {
            uint32_t val;
            memcpy(&val, data, sizeof(uint32_t));
            crc = __crc32w(crc, val); // 使用__crc32w
            data += 4;
        }

        // 处理2字节数据
        while (data + 2 <= end) {
            uint16_t val;
            memcpy(&val, data, sizeof(uint16_t));
            crc = __crc32h(crc, val); // 使用__crc32h
            data += 2;
        }

        // 处理1字节数据
        while (data < end) {
            uint8_t val = *data;
            crc = __crc32b(crc, val); // 使用__crc32b
            data += 1;
        }

        return crc;
    }
};

代码解析

  1. crc32_start:初始化CRC32为0xFFFFFFFF
  2. crc32_end:最终CRC32值为crc ^ 0xFFFFFFFF
  3. crc32_do:使用ARM的内置函数进行CRC32计算。按8字节、4字节、2字节和1字节的顺序处理数据,以提高性能。

性能压测

为了比较软件实现和硬件加速实现的CRC32计算性能,我们编写了一个性能压测程序。该程序包括:

  1. 数据生成:通过重复给定的字符串生成大规模测试数据。
  2. 一致性验证:确保软件和硬件实现的CRC32计算结果一致。
  3. 性能测量:使用高精度计时器(std::chrono)测量两种实现的执行时间。
  4. 结果报告:输出两种方法的执行时间和加速比。

完整的crc32_benchmark.cpp实现

// crc32_benchmark.cpp

// crc32_benchmark.cpp

#include <algorithm>
#include <arm_acle.h> // ARM 内置函数头文件
#include <chrono>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>

// 软件 CRC32 实现(逐字节、逐位计算,不使用查找表)
class SoftwareCRC32 {
public:
  static constexpr uint32_t CRC32_POLYNOMIAL = 0xEDB88320L;

  // 计算单个字节的 CRC32 值
  uint32_t calcCRC32Value(int32_t f_data_r) const {
    uint32_t ulCRC = f_data_r & 0xFF;
    for (int i = 0; i < 8; ++i) {
      if (ulCRC & 1)
        ulCRC = (ulCRC >> 1) ^ CRC32_POLYNOMIAL;
      else
        ulCRC >>= 1;
    }
    return ulCRC;
  }

  // 计算整个数据块的 CRC32
  uint32_t compute(const uint8_t *data, size_t len, uint32_t crc = 0) const {
    // 初始化 CRC32 为 0xFFFFFFFF
    crc = crc32_start();

    for (size_t i = 0; i < len; ++i) {
      uint8_t byte = data[i];
      uint32_t ulCrcDark = (crc >> 8) & 0x00FFFFFFL;
      uint32_t ulCrcWhite =
          calcCRC32Value(static_cast<int32_t>((crc ^ byte) & 0xFF));
      crc = ulCrcDark ^ ulCrcWhite;
    }

    // 最终 CRC32 为 crc ^ 0xFFFFFFFF
    crc = crc32_end(crc);
    return crc;
  }

  // 初始化 CRC32
  inline uint32_t crc32_start(void) const {
    return 0xFFFFFFFFU; // 标准 CRC32 初始化值
  }

  // 结束 CRC32
  inline uint32_t crc32_end(uint32_t crc) const {
    return crc ^ 0xFFFFFFFFU; // 标准 CRC32 最终异或
  }
};

// 硬件加速 CRC32 实现
class HardwareCRC32 {
public:
  // 初始化 CRC32
  inline uint32_t crc32_start(void) const {
    return 0xFFFFFFFFU; // 标准 CRC32 初始化值
  }

  // 结束 CRC32
  inline uint32_t crc32_end(uint32_t crc) const {
    return crc ^ 0xFFFFFFFFU; // 标准 CRC32 最终异或
  }

  // 使用 ARM 内置函数进行硬件加速的 CRC32 计算
  inline uint32_t crc32_do(const void *const in_buf, uint32_t crc,
                           const uint64_t in_buf_len) const {
    const uint8_t *data = static_cast<const uint8_t *>(in_buf);
    const uint8_t *end = data + in_buf_len;

    // 处理8字节数据
    while (data + 8 <= end) {
      uint64_t val;
      memcpy(&val, data, sizeof(uint64_t)); // 确保安全读取
      crc = __crc32d(crc, val);             // 使用 __crc32d
      data += 8;
    }

    // 处理4字节数据
    while (data + 4 <= end) {
      uint32_t val;
      memcpy(&val, data, sizeof(uint32_t));
      crc = __crc32w(crc, val); // 使用 __crc32w
      data += 4;
    }

    // 处理2字节数据
    while (data + 2 <= end) {
      uint16_t val;
      memcpy(&val, data, sizeof(uint16_t));
      crc = __crc32h(crc, val); // 使用 __crc32h
      data += 2;
    }

    // 处理1字节数据
    while (data < end) {
      uint8_t val = *data;
      crc = __crc32b(crc, val); // 使用 __crc32b
      data += 1;
    }

    return crc;
  }
};

// 生成测试数据
std::vector<uint8_t> generate_test_data(const std::string &base_str,
                                        size_t repeat_times) {
  std::vector<uint8_t> data;
  data.reserve(base_str.size() * repeat_times);
  for (size_t i = 0; i < repeat_times; ++i) {
    data.insert(data.end(), base_str.begin(), base_str.end());
  }
  return data;
}

int main() {
  using namespace std::chrono;

  // 定义测试字符串
  std::string test_str =
      "#RAWIMUA,COM4,0,100.0,UNKNOWN,1356,828.466,00000000,0000,782;"
      "1356,828.465612000,12800000,1309784,-55720,32705,-1006,-10274,6618*"
      "b290f1e0";

  // 定义重复次数以生成较大的数据块
  size_t repeat_times = 10; // 根据需要调整

  // 生成测试数据
  std::vector<uint8_t> test_data = generate_test_data(test_str, repeat_times);
  size_t data_size = test_data.size();
  std::cout << "Generated test data size: " << data_size << " bytes"
            << std::endl;

  // 准备软件 CRC32
  SoftwareCRC32 software_crc32;

  // 准备硬件加速 CRC32
  HardwareCRC32 hardware_crc32;

  // 定义迭代次数
  size_t iterations = 1000000; // 根据需要调整

  // 验证 CRC32 计算的一致性(小数据块)
  std::cout << "\nVerifying CRC32 consistency with small data block..."
            << std::endl;
  std::vector<uint8_t> small_test_data = generate_test_data(test_str, 1);
  uint32_t sw_crc_small =
      software_crc32.compute(small_test_data.data(), small_test_data.size());
  uint32_t hw_crc_small = hardware_crc32.crc32_do(small_test_data.data(),
                                                  hardware_crc32.crc32_start(),
                                                  small_test_data.size());
  hw_crc_small = hardware_crc32.crc32_end(hw_crc_small);
  std::cout << "Software CRC32 (small data): 0x" << std::hex << sw_crc_small
            << std::dec << std::endl;
  std::cout << "Hardware CRC32 (small data): 0x" << std::hex << hw_crc_small
            << std::dec << std::endl;
  if (sw_crc_small == hw_crc_small) {
    std::cout << "Verification Passed: CRC32 results match." << std::endl;
  } else {
    std::cout << "Verification Failed: CRC32 results do not match."
              << std::endl;
    return 1; // 终止程序
  }

  // 测试软件 CRC32 性能
  std::cout << "\nStarting software CRC32 benchmark..." << std::endl;
  auto start_sw = high_resolution_clock::now();
  uint32_t sw_crc_result = 0;
  for (size_t i = 0; i < iterations; ++i) {
    sw_crc_result = software_crc32.compute(test_data.data(), test_data.size(),
                                           sw_crc_result);
  }
  // 结束时间
  auto end_sw = high_resolution_clock::now();
  // 计算持续时间
  duration<double> duration_sw = end_sw - start_sw;
  // 输出结果
  std::cout << "Software CRC32 Result: 0x" << std::hex << sw_crc_result
            << std::dec << std::endl;
  std::cout << "Software CRC32 Time: " << duration_sw.count() << " seconds"
            << std::endl;

  // 测试硬件加速 CRC32 性能
  std::cout << "\nStarting hardware-accelerated CRC32 benchmark..."
            << std::endl;
  auto start_hw = high_resolution_clock::now();
  uint32_t hw_crc_result = 0;
  for (size_t i = 0; i < iterations; ++i) {
    hw_crc_result = hardware_crc32.crc32_do(
        test_data.data(), hardware_crc32.crc32_start(), test_data.size());
    hw_crc_result = hardware_crc32.crc32_end(hw_crc_result);
  }
  // 结束时间
  auto end_hw = high_resolution_clock::now();
  // 计算持续时间
  duration<double> duration_hw = end_hw - start_hw;
  // 输出结果
  std::cout << "Hardware CRC32 Result: 0x" << std::hex << hw_crc_result
            << std::dec << std::endl;
  std::cout << "Hardware CRC32 Time: " << duration_hw.count() << " seconds"
            << std::endl;

  // 计算速度提升
  double speedup = duration_sw.count() / duration_hw.count();
  std::cout << "\nSpeedup: " << speedup
            << "x faster using hardware-accelerated CRC32" << std::endl;

  return 0;
}

代码说明

  1. 软件CRC32实现

    • 采用逐字节、逐位计算的方法,不使用查找表。
    • calcCRC32Value函数计算单个字节的CRC32值。
    • compute函数计算整个数据块的CRC32值,遵循标准CRC32的初始化和最终异或步骤。
  2. 硬件加速CRC32实现

    • 利用ARMv8.1-A提供的内置函数__crc32d__crc32w__crc32h__crc32b分别处理8字节、4字节、2字节和1字节的数据。
    • 确保数据读取的安全性,通过memcpy避免未对齐访问。
  3. 性能压测流程

    • 数据生成:通过重复给定的字符串生成大规模测试数据(示例中为13,600,000字节)。
    • 一致性验证:使用较小的数据块(1360字节)验证软件和硬件实现的CRC32结果是否一致。
    • 性能测量
      • 对软件和硬件实现分别进行1000次迭代的CRC32计算,记录总执行时间。
      • 计算并输出两种方法的执行时间及加速比。

编译与运行

确保您的编译器支持ARMv8.1-A及其CRC32扩展,并启用相应的编译选项。例如,使用GCC或Clang时,可以使用以下编译命令:

aarch64-linux-gnu-g++ -march=armv8.1-a+crc -O3 -o crc32_benchmark crc32_benchmark.cpp

运行结果示例

以下是一个示例运行结果:

Generated test data size: 13600000 bytes

Verifying CRC32 consistency with small data block...
Software CRC32 (small data): 0xb290f1e0
Hardware CRC32 (small data): 0xb290f1e0
Verification Passed: CRC32 results match.

Starting software CRC32 benchmark...
Software CRC32 Result: 0x5c6092a8
Software CRC32 Time: 20.1918 seconds

Starting hardware-accelerated CRC32 benchmark...
Hardware CRC32 Result: 0x5c6092a8
Hardware CRC32 Time: 0.184211 seconds

Speedup: 109.612x faster using hardware-accelerated CRC32

结果分析

  1. 一致性验证

    • 软件和硬件加速实现的CRC32结果一致,确保两者的正确性。
  2. 性能压测

    • 软件实现:处理13,600,000字节的数据块1000次,共耗时约20.19秒。
    • 硬件加速实现:处理相同数据块1000次,仅耗时约0.184秒。
    • 加速比:硬件加速实现比软件实现快约110倍。

性能提升原因

ARM的CRC32指令集通过硬件级别的并行处理和优化,大幅提升了CRC32计算的效率。相比于逐位计算的软件实现,硬件加速实现能够利用处理器的专用指令,减少大量的循环和位操作,从而显著缩短计算时间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2206204.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux的kafka安装部署

1.kafka是一个分布式的,去中心化的,高吞吐低延迟,订阅模式的消息队列系统 确保要有jdk与zookeeper安装配置 2.下载kafka安装包 http://archive.apache.org/dist/kafka/2.4.1/kafka_2.12-2.4.1.tgz 此时可以wget http://archive.apache.org/dist/kafka/2.4.1/kafka_2.12-2.4.…

数字后端零基础入门系列 | Innovus零基础LAB学习Day1

一 Floorplan 数字IC后端设计如何从零基础快速入门&#xff1f;(内附数字IC后端学习视频&#xff09; Lab5-1这个lab学习目标很明确——启动Innovus工具并完成设计的导入。 在进入lab之前&#xff0c;我们需要进入我们的FPR工作目录。 其中ic062为个人服务器账户。比如你端…

Graph Contrastive Learningwith Reinforcement Augmentation

IJCAI24 推荐指数&#xff1a; #paper/⭐⭐⭐ 领域&#xff1a;图增强强化学习 胡言乱语&#xff1a; 不愧是清华组的论文&#xff0c;这个实验的目的是利用强化学习去生成对比学习的增强视图。但是&#xff0c;其仍然有一些小问题&#xff1a;其本质实际是对以往的图增强方法…

vrrp实验

配置Trunk和Access [SW3]int e0/0/1 [SW3-Ethernet0/0/1]p l a [SW3-Ethernet0/0/1]p d v 10 [SW3-Ethernet0/0/1]int e0/0/2 [SW3-Ethernet0/0/2]p l a [SW3-Ethernet0/0/2]p d v 10 [SW3-Ethernet0/0/2]int e0/0/3 [SW3-Ethernet0/0/3]p l a [SW3-Ethernet0/0/3]p d v 20 [S…

postgresql 安装

一、下载 PostgreSQL: File Browser 下载地址 PostgreSQL: File Browser 上传到服务器,并解压 二、安装依赖 yum install -y perl-ExtUtils-Embed readline-devel zlib-devel pam-devel libxml2-devel libxslt-devel openldap-devel 创建postgresql 和目录 useradd …

位运算 -- 力扣

1486. 数组异或操作 1486. 数组异或操作 根据题意&#xff0c;使用参数 n 和 start 生成一个数组&#xff0c;最后返回数组中所有元素按位异或&#xff08;XOR&#xff09;后得到的结果。 首先&#xff0c;异或运算的规则是&#xff0c;当同一位的二进制数不同时结果为 1&…

【Golang】Go 语言中的 time 包详解:全面掌握时间处理与应用

在 Go 语言中&#xff0c;time 包提供了强大的时间处理功能&#xff0c;适用于各种场景&#xff1a;获取当前时间、格式化和解析时间、计算时间间隔、设置定时器、处理超时等。在开发过程中&#xff0c;熟练掌握 time 包能够帮助我们轻松处理时间相关的操作&#xff0c;尤其是定…

小程序之获取用户头像与昵称

html 这个是通过一个点击事件进行显示的,下面是效果图,获取头像最关键的是 open-type"chooseAvatar" ,获取昵称最关键的是 type"nickname" ,大家多加注意i哦 <!-- 大的输入调取 --><view class"myis_box" wx:if"{{showMyisBox}}…

做ppt用什么软件好?5个办公必备的ppt工具推荐!

ppt用什么软件做&#xff1f; 相信很多人看到这个&#xff0c;会第一时间想到Microsoft Office套件包含的幻灯片软件Powerpoint&#xff0c;它的名声甚广&#xff0c;以至于某种程度上成了PPT的代名词。 在Powerpoint之外&#xff0c;这些年也陆续诞生了各式各样的PPT软件&am…

CocosCreator 快速部署 TON 游戏:Web2 游戏如何使用 Ton支付

在本篇文章中&#xff0c;我们将继续探讨如何使用 Cocos Creator 开发 Telegram 游戏&#xff0c;重点介绍如何集成 TON 支付功能。通过这一教程&#xff0c;开发者将学会如何在游戏中接入 TON Connect&#xff0c;实现钱包连接、支付以及支付后的校验流程&#xff0c;最终为 W…

YOLO11改进|SPPF篇|引入SPPFCSPC金字塔结构

目录 一、【SPPFCSPC】金字塔结构1.1【SPPFCSPC】金字塔结构介绍1.2【SPPFCSPC】核心代码 二、添加【SPPFCSPC】金字塔结构2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、【SPPFCSPC】金字塔结构 1.1【SPPFCSPC】金字塔结构介绍 下图是…

vue后台管理系统从0到1(1)

文章目录 vue后台管理系统从0到1&#xff08;1&#xff09;nvm 下载安装1.卸载nodejs环境2.安装nvm 安装nrm vue后台管理系统从0到1&#xff08;1&#xff09; 第一节主要是先安装我们的工具nvm nodejs版本管理工具&#xff0c;和nrm镜像管理工具 nvm 下载安装 nvm是一款管理…

网络流量预测的学习——持续更新ing

文章目录 前情提要何为网络流量网络流量分析&#xff08;NTA&#xff09;网络流量组成网络流量处理过程 预测网络流量的工具wiresharkbrim&#xff08;zui&#xff09; 机器学习中的网络流量预测参考文章 前情提要 记录一些有关网络流量的学习 何为网络流量 网络流量是指在计…

【D3.js in Action 3 精译_033】4.1.0 DIY 实战:如何通过学习 d3.autoType 函数深度参与 D3 生态建设

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

又被特斯拉演了?继续“画饼式”发布Robotaxi,产业链静观其变

9月底的暴涨后&#xff0c;A股资产正经历回调&#xff0c;科技板块变现参差。不过&#xff0c;无人驾驶领域的预期依然很强。 10月10日科技股全线调整之际&#xff0c;无人驾驶板块盘中的巨幅震荡拉升就是典型的预热动作。东箭科技、天龙股份等多只智能驾驶个股涨停。核心驱动…

ACR、PZ、AMC仪表接线说明及通讯协议解析

1.ACR/PZ/AMC多功能表接线说明 三相三线接线说明 使用场合负载是平衡系统&#xff0c;并且没有零线的场合。 1. 端子号1&#xff0c;2为辅助电源&#xff1a; 如上图&#xff0c;接入相电压220V输入。其中辅助电源的火线加装5A保险丝&#xff0c;零线直接接到零排上。 2&am…

iPhone使用指南:如何在没有备份的情况下从 iPhone 恢复已删除的照片

本指南将向您展示如何在没有备份的情况下从 iPhone 恢复已删除的照片。我们所有人在生活中的某个时刻都一定做过一些愚蠢的事情&#xff0c;例如从手机或电脑中删除一些重要的东西。这是很自然的&#xff0c;没有什么可羞耻的。您可能在辛苦工作一天后回来。当突然想看一些照片…

C++开发五子棋游戏案例详解

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

JavaSE——集合3:ArrayList、Vector

目录 一、ArrayList的注意事项 二、ArrayList的扩容机制(重要) 三、Vector底层结构和源码剖析 1.Vector类的定义说明 2.Vector底层也是一个对象数组 3.Vector是线程同步的&#xff0c;即线程安全&#xff0c;Vector类的操作方法带有synchronized 4.在开发中&#xff0c…

obs录屏怎么样?四大优秀录屏工具亲测好用!

录屏需求日盛&#xff0c;接下来我们就来聊聊几款市面上较为热门的录屏软件——福昕录屏大师、转转大师录屏、爱拍录屏以及经典的obs录屏&#xff0c;希望能给寻找合适录屏工具的您带来一些灵感。 福昕录屏大师 直达链接&#xff1a;www.foxitsoftware.cn/REC/ 如果你刚开始…