龙芯爱好者的日常!
返回

龙芯LoongArch指令集计算CRC32的速度是MIPS的4倍以上

2021-09-01 LoongArch 龙芯 生态 CRC32 汇编 165 0



相信凡是与编程或通信相关的技术人员都知道CRC32,这是一种常用的数据校验编码。因为CRC32编码的算法很简单,所以用于计算CRC32值的C/C++代码写法基本上固定,很难有优化的余地。通常大家都通过手写汇编,调用特定指令集用于CRC32计算的专用指令来达到计算加速的目的。龙芯以前使用的MIPS以及龙芯自己的扩展指令中都没有CRC32计算指令,现在全新的LoongArch指令集中提供了计算CRC32的专用指令,使计算CRC32的速度达到原来的4倍以上。

使用专用指令达到提速的目的其实没什么可说的,但如果不使用专用指令,而仅仅是把C/C++代码编译为LoongArch的二进制,就能比MIPS快20%以上呢?是不是就比较有意思了!

下面是一段计算CRC32值的C程序,大多数计算CRC32的代码都应该和它相似。函数CrcGenerateTable用于初始化计算多项式表,只需要调用一次。函数CrcUpdate用于计算数据的CRC32值,只有短短的几行。

#include "stdlib.h"
#include "stdio.h"
#include "time.h"
typedef unsigned char Byte;
typedef unsigned int UInt32;
typedef unsigned long long UInt64;
#define kCrcPoly 0xEDB88320
#define CRC_INIT_VAL 0xFFFFFFFF
UInt32 g_CrcTable[256];
void CrcGenerateTable()
{
    UInt32 i;
    for (i = 0; i < 256; i++)
    {
        UInt32 r = i;
        unsigned j;
        for (j = 0; j < 8; j++)
            r = (r >> 1) ^ (kCrcPoly & ~((r & 1) - 1));
        g_CrcTable[i] = r;
    }
}

UInt32 CrcUpdate(UInt32 v, const void *data, UInt64 size)
{
    const Byte *p = (const Byte *)data;
    const Byte *pEnd = p + size;
    for (; p != pEnd; p++)
        v = g_CrcTable[(v ^ *p) & 0xFF] ^ (v >> 8);
    return v;
}

我们先使用GCC编译器 -O参数编译,看看CrcUpdate这个函数分别编译为LoongArch和MIPS有什么区别:

龙芯LoongArch指令集计算CRC32的速度是MIPS的4倍以上


第一眼看到的,就是实现相同功能时,LoongArch二进制的指令数量要比MIPS少很多。然后呢,就是两者的指令命名规则和汇编格式有明显的差异。最重要的就是它们的二进制操作码完全没有相似之处,说明这是两种不同的指令集,无法二进制兼容。最近有一些缺乏专业素养的媒体以及公司说LoongArch只是对MIPS的扩展,把“C/C++源代码兼容”和“指令集二进制兼容”混为一谈,还弄不清楚“二进制兼容”和“二进制翻译方式兼容”的区别,不知道他们怎么好意思自称技术博主和科技公司?

上面的二进制程序都是用-O参数编译的,是一种编译器默认的编译优化参数。我在龙芯3A5000和3A4000上分别使用能使它们较好发挥性能的编译参数,对这段计算CRC32的C语言程序进行测试,发现性能差距很大。

编码1GB的数据,使用MIPS指令集的3A4000(1.8GHz)耗时6.57秒(单线程),平均每秒约156M字节。使用LoongArch指令集的3A5000(耗时)3.87秒(单线程),平均每秒约264MB字节。3A4000和3A5000的CPU主频有差距,但CPU核心基本一样。如果把这个数据折算到相同的CPU频率,3A5000仍然比3A4000高出22.2%。按数据量和时间算,内存也不可能是瓶颈(实测单通道和双通道没有差距),它们最大的区别就只剩下指令集了。从上面的编译结果就能看出来,LoongArch实现相同功能使用的指令更少,这就是全新LoongArch指令集的优势之一了。即便使用优化的编译参数,结果也一样,MIPS指令数量无法比LoongArch更少。

上面提到使用LoongArch中的CRC专用指令可以超过“4倍性能”,这也需要展示一番,不然本文就是不完整的。我编写了一个与CrcUpdate这个函数功能相同的LoongArch汇编函数,用它来与C/C++代码的编译结果进行对比。先看测试结果,然后我再解析汇编代码。

龙芯LoongArch指令集计算CRC32的速度是MIPS的4倍以上


当把实测的CRC32编码速度都折算到1GHz之后,使用LoongArch指令集的3A5000仍有超过350MB/s的编码速度(单线程),是使用MIPS指令集的3A4000的CRC32编码速度的4倍以上。实际上对汇编代码仍然可以继续优化,在我进行了一些优化后,3A5000的CRC32编码速度超过了1000MB/s。

使用LoongArch中的CRC指令就不需要事先计算多项式表了,CPU中内置了IEEE.802.3多项式(多项式值为0xEDB88320),和Castagnoli多项式(多项式值为0x82F63B78)的表。当使用“crc”为指令前缀时,使用就的是IEEE.802.3多项式。当使用“crcc”为指令前缀时,使用的就是Castagnoli多项式。

  其实汇编代码也不复杂,毕竟只是一个很小的函数,几乎每一行我都加了详细注释,因为这也是我自己学习的过程。因为贴代码容易格式错乱,只好贴成图片。

龙芯LoongArch指令集计算CRC32的速度是MIPS的4倍以上

从上面可以看到,L3标签下面只有4条指令,这个最主要的循环中有2条指令是用于处理循环条件的。那么继续优化的方法就呼之欲出,只要在一次循环中多计算几次,就可以减少循环次数。“ldptr.d $r12,$r5,0”这条载入数据的指令中,最后一个参数是立即数,作为取数据的偏移量,于是如果把循环代码改成下面的样子,每次循环就可以计算16个字节:

龙芯LoongArch指令集计算CRC32的速度是MIPS的4倍以上

以此类推,也可以计算24、32、48……个字节。这样修改之后,代码中还有其它地方也需要做修改,比如函数入口处的8字节对齐计算就修改成对应的数。下面的单字节循环也需要修改,不过也有偷懒的方法,比如想每次循环计算64字节,就单独写一个只计算数据长度为64倍数的函数。当数据长度大于等于64时,就可以先调用它一次,把剩下的数据再调用之前的函数来计算,就像下面这样:

龙芯LoongArch指令集计算CRC32的速度是MIPS的4倍以上

LoongArch指令集中的CRC指令并不只是可以计算单字节和8字节,它其实有计算1、2、4、8字节的4种格式,后缀分别为“w.b.w”、“w.h.w”、“w.w.w”、“w.d.w”。

龙芯LoongArch指令集中除了CRC指令之外,还有许多有趣的指令。比如字节序翻转指令,在处理网络通信数据、图像像素格式转换等的时候就能用得上。比如取得一个数中有多少个bit为1,在处理mask数据时就非常有用。LoongArch中的原子访存指令特别丰富,在做多线程互斥、数据“无锁”访问等功能时就可以比较灵活。

龙芯LoongArch指令集中各种有趣的指令和有趣的用法,大家一起去发掘吧。《龙芯架构参考手册》您值得拥有!




顶部