NEON 指令集对 CRC32 加速明显,但在 CRC 计算中反而造成性能下降的分析

0. 概要

本文比较了使用 NEON 指令集和简单 C 循环实现 CRC 和 CRC32 校验的性能。结果表明,启用 NEON 指令集可以显著提高 CRC32 的性能,而对 CRC 的影响则相反。在 -O2 编译优化的情况下,NEON CRC32 的速度比简单 C 循环快 8 倍以上,而 NEON CRC 竟然出乎意料的比简单 C 循环慢 20%。
本文就此现象分析原因。

1. 实验方法

为了评估 NEON 指令集对 CRC 和 CRC32 的影响,我们编写了两种实现:一种使用 NEON 指令集进行优化,另一种采用简单的 C 循环。

1.1 NEON指令集尝试加速CRC和简单CRC对比

#include <arm_neon.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define DATA_SIZE 1024 * 1024  // 1MB的数据大小

bool crc_NEON(uint8_t* data, uint32_t len) {
  int sum = 0;
  uint32_t i = 1;

  // 使用NEON指令进行累加操作
  uint8x16_t v_sum = vdupq_n_u8(0);  // 初始化向量寄存器为0

  // 每次处理16个字节
  for (; i + 16 <= len; i += 16) {
    uint8x16_t v_data = vld1q_u8(data + i);  // 加载16个字节
    v_sum = vaddq_u8(v_sum, v_data);         // 累加
  }

  // 将向量寄存器中的各个字节累加到sum
  uint8_t temp[16];
  vst1q_u8(temp, v_sum);
  for (int j = 0; j < 16; j++) {
    sum += temp[j];
  }

  // 处理剩余的字节
  for (; i < len; i++) {
    sum += data[i];
  }

  // 计算校验和并与数据的最后一个字节进行比较
  if ((uint8_t)((0x100 - (sum & 0xff)) & 0xff) == data[len]) {
    return true;
  }
  return false;
}

bool crc_Simple(uint8_t* data, uint32_t len) {
  int sum = 0;
  for (uint32_t i = 1; i < len; i++) {
    sum += data[i];
  }
  if ((uint8_t)((0x100 - (sum & 0xff)) & 0xff) == data[len]) {
    return true;
  }
  return false;
}

int main() {
  // 初始化随机数据
  uint8_t* data = (uint8_t*)aligned_alloc(16, DATA_SIZE + 1);  // 确保16字节对齐
  if (!data) {
    printf("内存分配失败\n");
    return -1;
  }
  srand(time(NULL));
  for (uint32_t i = 0; i < DATA_SIZE; i++) {
    data[i] = rand() % 256;
  }
  data[DATA_SIZE] = rand() % 256;  // 最后一个字节作为校验和字节

  // 测试NEON实现的性能
  clock_t start_time = clock();
  crc_NEON(data, DATA_SIZE);
  clock_t end_time = clock();
  double time_taken = ((double)(end_time - start_time)) / CLOCKS_PER_SEC * 1000000;  // 转换为微秒
  printf("NEON crc耗时: %f 微秒\n", time_taken);

  // 测试简单实现的性能
  start_time = clock();
  crc_Simple(data, DATA_SIZE);
  end_time = clock();
  time_taken = ((double)(end_time - start_time)) / CLOCKS_PER_SEC * 1000000;  // 转换为微秒
  printf("简单crc耗时: %f 微秒\n", time_taken);

  // 释放内存
  free(data);

  return 0;
}

/*
  /path/aarch64-linux-gcc -O2 -march=armv8-a+simd -o crc_test main.c
*/


在 ARM64开发板执行多次的测试结果

# 未开启编译器优化 -O0
test@t:/userdata# ./crc32_test
NEON crc耗时: 1562.000000 微秒
简单crc耗时: 11247.000000 微秒
test@t:/userdata# ./crc32_test
NEON crc耗时: 1488.000000 微秒
简单crc耗时: 11120.000000 微秒
test@t:/userdata# ./crc32_test
NEON crc耗时: 1505.000000 微秒
简单crc耗时: 11131.000000 微秒

# 开启编译器优化 -O2
test@t:/userdata# ./crc32_test
NEON crc耗时: 815.000000 微秒
简单crc耗时: 5.000000 微秒
test@t:/userdata# ./crc32_test
NEON crc耗时: 844.000000 微秒
简单crc耗时: 6.000000 微秒
test@t:/userdata# ./crc32_test
NEON crc耗时: 710.000000 微秒
简单crc耗时: 3.000000 微秒
test@t:/userdata# ./crc32_test
NEON crc耗时: 632.000000 微秒
简单crc耗时: 4.000000 微秒

1.2 NEON指令集加速CRC32和普通CRC32对比

// set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=generic+crc")
// set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=generic+crc")
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define DATA_SIZE (1024 * 1024)  // 1MB 数据大小
#define kCrc32TableSize 256

uint32_t crc32_start(void) {
  return 0xffffffffU;
}

uint32_t crc32_end(uint32_t crc) {
  return crc ^ 0xffffffffU;
}

uint32_t crc32_do_NENO(const void *const in_buf, uint32_t crc,
                       const uint64_t in_buf_len) {
  int64_t bytes = in_buf_len;
  const uint8_t *data = (const uint8_t *)(in_buf);
  while (bytes >= sizeof(uint64_t)) {
    __asm__("crc32cx %w[c], %w[c], %x[v]"
            : [c] "+r"(crc)
            : [v] "r"(*((uint64_t *)data)));
    data += sizeof(uint64_t);
    bytes -= sizeof(uint64_t);
  }
  if (bytes & sizeof(uint32_t)) {
    __asm__("crc32cw %w[c], %w[c], %w[v]"
            : [c] "+r"(crc)
            : [v] "r"(*((uint32_t *)data)));
    data += sizeof(uint32_t);
    bytes -= sizeof(uint32_t);
  }
  if (bytes & sizeof(uint16_t)) {
    __asm__("crc32ch %w[c], %w[c], %w[v]"
            : [c] "+r"(crc)
            : [v] "r"(*((uint16_t *)data)));
    data += sizeof(uint16_t);
    bytes -= sizeof(uint16_t);
  }
  if (bytes & sizeof(uint8_t)) {
    __asm__("crc32cb %w[c], %w[c], %w[v]"
            : [c] "+r"(crc)
            : [v] "r"(*((uint8_t *)data)));
  }
  return crc;
}

// from:
// https://github.com/andry81/
// tacklelib/blob/34ae9f73f5fb209ad8a5d7e9817e90979680efc6/
// include/tacklelib/utility/crc_tables.hpp
// Table of g_crc32_1EDC6F41
static uint32_t crctable[kCrc32TableSize] = {
    0x00000000U, 0xf26b8303U, 0xe13b70f7U, 0x1350f3f4U, 0xc79a971fU,
    0x35f1141cU, 0x26a1e7e8U, 0xd4ca64ebU, 0x8ad958cfU, 0x78b2dbccU,
    0x6be22838U, 0x9989ab3bU, 0x4d43cfd0U, 0xbf284cd3U, 0xac78bf27U,
    0x5e133c24U, 0x105ec76fU, 0xe235446cU, 0xf165b798U, 0x030e349bU,
    0xd7c45070U, 0x25afd373U, 0x36ff2087U, 0xc494a384U, 0x9a879fa0U,
    0x68ec1ca3U, 0x7bbcef57U, 0x89d76c54U, 0x5d1d08bfU, 0xaf768bbcU,
    0xbc267848U, 0x4e4dfb4bU, 0x20bd8edeU, 0xd2d60dddU, 0xc186fe29U,
    0x33ed7d2aU, 0xe72719c1U, 0x154c9ac2U, 0x061c6936U, 0xf477ea35U,
    0xaa64d611U, 0x580f5512U, 0x4b5fa6e6U, 0xb93425e5U, 0x6dfe410eU,
    0x9f95c20dU, 0x8cc531f9U, 0x7eaeb2faU, 0x30e349b1U, 0xc288cab2U,
    0xd1d83946U, 0x23b3ba45U, 0xf779deaeU, 0x05125dadU, 0x1642ae59U,
    0xe4292d5aU, 0xba3a117eU, 0x4851927dU, 0x5b016189U, 0xa96ae28aU,
    0x7da08661U, 0x8fcb0562U, 0x9c9bf696U, 0x6ef07595U, 0x417b1dbcU,
    0xb3109ebfU, 0xa0406d4bU, 0x522bee48U, 0x86e18aa3U, 0x748a09a0U,
    0x67dafa54U, 0x95b17957U, 0xcba24573U, 0x39c9c670U, 0x2a993584U,
    0xd8f2b687U, 0x0c38d26cU, 0xfe53516fU, 0xed03a29bU, 0x1f682198U,
    0x5125dad3U, 0xa34e59d0U, 0xb01eaa24U, 0x42752927U, 0x96bf4dccU,
    0x64d4cecfU, 0x77843d3bU, 0x85efbe38U, 0xdbfc821cU, 0x2997011fU,
    0x3ac7f2ebU, 0xc8ac71e8U, 0x1c661503U, 0xee0d9600U, 0xfd5d65f4U,
    0x0f36e6f7U, 0x61c69362U, 0x93ad1061U, 0x80fde395U, 0x72966096U,
    0xa65c047dU, 0x5437877eU, 0x4767748aU, 0xb50cf789U, 0xeb1fcbadU,
    0x197448aeU, 0x0a24bb5aU, 0xf84f3859U, 0x2c855cb2U, 0xdeeedfb1U,
    0xcdbe2c45U, 0x3fd5af46U, 0x7198540dU, 0x83f3d70eU, 0x90a324faU,
    0x62c8a7f9U, 0xb602c312U, 0x44694011U, 0x5739b3e5U, 0xa55230e6U,
    0xfb410cc2U, 0x092a8fc1U, 0x1a7a7c35U, 0xe811ff36U, 0x3cdb9bddU,
    0xceb018deU, 0xdde0eb2aU, 0x2f8b6829U, 0x82f63b78U, 0x709db87bU,
    0x63cd4b8fU, 0x91a6c88cU, 0x456cac67U, 0xb7072f64U, 0xa457dc90U,
    0x563c5f93U, 0x082f63b7U, 0xfa44e0b4U, 0xe9141340U, 0x1b7f9043U,
    0xcfb5f4a8U, 0x3dde77abU, 0x2e8e845fU, 0xdce5075cU, 0x92a8fc17U,
    0x60c37f14U, 0x73938ce0U, 0x81f80fe3U, 0x55326b08U, 0xa759e80bU,
    0xb4091bffU, 0x466298fcU, 0x1871a4d8U, 0xea1a27dbU, 0xf94ad42fU,
    0x0b21572cU, 0xdfeb33c7U, 0x2d80b0c4U, 0x3ed04330U, 0xccbbc033U,
    0xa24bb5a6U, 0x502036a5U, 0x4370c551U, 0xb11b4652U, 0x65d122b9U,
    0x97baa1baU, 0x84ea524eU, 0x7681d14dU, 0x2892ed69U, 0xdaf96e6aU,
    0xc9a99d9eU, 0x3bc21e9dU, 0xef087a76U, 0x1d63f975U, 0x0e330a81U,
    0xfc588982U, 0xb21572c9U, 0x407ef1caU, 0x532e023eU, 0xa145813dU,
    0x758fe5d6U, 0x87e466d5U, 0x94b49521U, 0x66df1622U, 0x38cc2a06U,
    0xcaa7a905U, 0xd9f75af1U, 0x2b9cd9f2U, 0xff56bd19U, 0x0d3d3e1aU,
    0x1e6dcdeeU, 0xec064eedU, 0xc38d26c4U, 0x31e6a5c7U, 0x22b65633U,
    0xd0ddd530U, 0x0417b1dbU, 0xf67c32d8U, 0xe52cc12cU, 0x1747422fU,
    0x49547e0bU, 0xbb3ffd08U, 0xa86f0efcU, 0x5a048dffU, 0x8ecee914U,
    0x7ca56a17U, 0x6ff599e3U, 0x9d9e1ae0U, 0xd3d3e1abU, 0x21b862a8U,
    0x32e8915cU, 0xc083125fU, 0x144976b4U, 0xe622f5b7U, 0xf5720643U,
    0x07198540U, 0x590ab964U, 0xab613a67U, 0xb831c993U, 0x4a5a4a90U,
    0x9e902e7bU, 0x6cfbad78U, 0x7fab5e8cU, 0x8dc0dd8fU, 0xe330a81aU,
    0x115b2b19U, 0x020bd8edU, 0xf0605beeU, 0x24aa3f05U, 0xd6c1bc06U,
    0xc5914ff2U, 0x37faccf1U, 0x69e9f0d5U, 0x9b8273d6U, 0x88d28022U,
    0x7ab90321U, 0xae7367caU, 0x5c18e4c9U, 0x4f48173dU, 0xbd23943eU,
    0xf36e6f75U, 0x0105ec76U, 0x12551f82U, 0xe03e9c81U, 0x34f4f86aU,
    0xc69f7b69U, 0xd5cf889dU, 0x27a40b9eU, 0x79b737baU, 0x8bdcb4b9U,
    0x988c474dU, 0x6ae7c44eU, 0xbe2da0a5U, 0x4c4623a6U, 0x5f16d052U,
    0xad7d5351U};

uint32_t crc32_do_simple(const void *const in_buf, uint32_t crc,
                         const uint64_t in_buf_len) {
  const uint8_t *data = (const uint8_t *)(in_buf);
  uint64_t bytes = in_buf_len;
  const uint64_t loop_limit_count = 4;
  // Calculate the crc for the data
  while (bytes >= loop_limit_count) {
    // Calculate the crc for 4 bytes
    crc ^= (uint32_t)(*data++) & 0xFFU;
    crc ^= ((uint32_t)(*data++) & 0xFFU) << 8;
    crc ^= ((uint32_t)(*data++) & 0xFFU) << 16;
    crc ^= ((uint32_t)(*data++) & 0xFFU) << 24;

    // Update the crc
    crc = crctable[crc & 0xFFU] ^ (crc >> 8);
    crc = crctable[crc & 0xFFU] ^ (crc >> 8);
    crc = crctable[crc & 0xFFU] ^ (crc >> 8);
    crc = crctable[crc & 0xFFU] ^ (crc >> 8);

    bytes -= 4;
  }

  // Handle any remaining bytes
  while (bytes != 0) {
    bytes--;
    crc = crctable[(crc ^ (uint32_t)(*data++)) & 0xFFU] ^ (crc >> 8);
  }

  return crc;
}

int main() {
  // 初始化随机数据
  uint8_t *data = (uint8_t *)malloc(DATA_SIZE);
  if (!data) {
    printf("内存分配失败\n");
    return -1;
  }
  srand(time(NULL));
  for (uint32_t i = 0; i < DATA_SIZE; i++) {
    data[i] = rand() % 256;
  }

  // 压力测试开始

  clock_t start_time, end_time;
  double time_taken;
  uint32_t crc;

  // 测试NEON实现的性能
  start_time = clock();
  crc = crc32_start();
  crc = crc32_do_NENO(data, crc, DATA_SIZE);
  crc = crc32_end(crc);
  end_time = clock();
  time_taken = ((double)(end_time - start_time)) / CLOCKS_PER_SEC *
               1000000;  // 转换为微秒
  printf("NEON CRC32耗时: %f 微秒\n", time_taken);

  // 测试简单实现的性能
  start_time = clock();
  crc = crc32_start();
  crc = crc32_do_simple(data, crc, DATA_SIZE);
  crc = crc32_end(crc);
  end_time = clock();
  time_taken = ((double)(end_time - start_time)) / CLOCKS_PER_SEC *
               1000000;  // 转换为微秒
  printf("simple CRC32耗时: %f 微秒\n", time_taken);

  // 释放内存
  free(data);

  return 0;
}

在 ARM64开发板执行多次的测试结果

# 执行多次的测试结果:
# 未开启编译器优化-O0
test@t:/userdata# crc32_test
NEON CRC32耗时: 1265.000000 微秒
simple CRC32耗时: 13898.000000 微秒
test@t:/userdata# crc32_test
NEON CRC32耗时: 1295.000000 微秒
simple CRC32耗时: 14325.000000 微秒
test@t:/userdata# crc32_test
NEON CRC32耗时: 1256.000000 微秒
simple CRC32耗时: 13924.000000 微秒
test@t:/userdata# crc32_test
NEON CRC32耗时: 1307.000000 微秒
simple CRC32耗时: 13916.000000 微秒

# 未开启编译器优化-O2
test@t:/userdata# ./crc32_test
NEON CRC32耗时: 700.000000 微秒
simple CRC32耗时: 4796.000000 微秒
test@t:/userdata# ./crc32_test
NEON CRC32耗时: 730.000000 微秒
simple CRC32耗时: 4810.000000 微秒
test@t:/userdata# ./crc32_test
NEON CRC32耗时: 540.000000 微秒
simple CRC32耗时: 4819.000000 微秒
test@t:/userdata# ./crc32_test
NEON CRC32耗时: 791.000000 微秒
simple CRC32耗时: 4867.000000 微秒

1.3 -O2 编译优化结果

  • NEON CRC32耗时: 700.000000 微秒
  • 简单实现 CRC32耗时: 4796.000000 微秒

1.4 未开启 -O2 优化结果

  • NEON CRC32耗时: 1265.000000 微秒
  • 简单实现 CRC32耗时: 13898.000000 微秒

2. NEON 加速原理

NEON 加速 CRC 和 CRC32 的原理是利用 SIMD(单指令多数据)技术,一次性处理多个数据元素。对于 CRC32 来说,NEON 提供了 vmulq_u8vaddq_u8vxorq_u8 等指令,可以并行处理 8 个字节的数据,从而显著提高计算效率。

具体来说,NEON CRC32 的实现步骤如下:

  • 初始化一个向量寄存器为 0xFFFFFFFF。
  • 使用 vld1q_u8 指令加载 8 个字节的数据到向量寄存器中。
  • 使用 vmulq_u8 指令将每个数据元素与 CRC32 多项式相乘。
  • 使用 vaddq_u8 指令将乘积累加到结果向量寄存器中。
  • 使用 vxorq_u8 指令将结果向量寄存器中的各个元素进行异或操作。
  • 重复步骤 2 到 5,直到所有数据都被处理。
  • 将结果向量寄存器中的各个元素累加到一个标量变量中。
  • 将标量变量中的值与预期的 CRC32 校验和进行比较。

3. 性能分析

实验结果表明,NEON CRC32 的性能明显优于简单 C 循环。这是因为 NEON 指令集可以并行处理多个数据元素,而简单 C 循环只能逐个处理数据元素。对于 CRC32 来说,NEON 指令集可以一次性处理 8 个字节的数据,相当于将每条指令的执行效率提高了 8 倍。

此外,NEON CRC32 的实现使用了查表技术,进一步提高了计算效率。查表技术是利用预先计算好的结果来代替繁重的计算过程。对于 CRC32 来说,查表技术可以将每次乘法运算替换为一次表查找操作,从而显著降低计算成本。

然而,对于 CRC 来说,NEON 指令集反而降低了性能。这是因为 CRC 的计算过程比较简单,NEON 指令集的并行处理优势无法充分发挥。此外,NEON CRC 的实现需要额外的指令来加载和存储数据,这也会增加一定的开销。

4. 结论

总体而言,NEON 指令集可以显著提高 CRC32 的性能,但对 CRC 的影响则相反。在实际应用中,应根据具体情况选择合适的 CRC 实现方式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值