dr2001 发表于 2013-1-6 09:37:25

[代码][C库]针对32Bit ARM优化的CRC库,静态配置支持大部分CRC。

本帖最后由 dr2001 于 2013-1-6 09:46 编辑

简要说明:
1 参考文献:"A Painless Guide To CRC Error Detection Algorithms"。Google即可。

2 算法信息:
2-1 支持大部分CRC:CRC3到CRC32,输入和输出同序。即宽于CRC32的不支持;同序指输入和输出要么都反射,要么都不反射。否则结果需要自行反射处理一下。
2-2 CRC半字节查表法。表大小固定为64Bytes,无论是计算哪种CRC。(CRC3 .. CRC32)
2-3 算法C代码针对ARM 32Bit架构(ARM/Thumb2)优化。在ARM926EJ-S和Cortex-M3上进行过反汇编验证。可以用于别的体系结构,但效率会低。
2-4 计算结果和第三方软件计算结果以及CRC算法标准CheckNumber校对过。主要测试过CRC,MSB/LSB都有。

3 使用说明:
库仅包含一个C文件,每个C文件通过宏进行静态配置;每个C文件支持一种CRC。
3-1 复制C文件为特定的文件名。
3-2 修改CRC_FUNC_NAME, CRC_WIDTH, CRC_DIRCT, CRC_POLY, CRC_INIT, CRC_XORV这几个宏定义。
    其中FUNC Name是函数名,其它的是CRC配置信息。
3-3 如果使用的不是C99编译器,需要自行声明对应长度的变量类型。
3-4 编译该C文件,自行在h文件中声明对应的函数名,即可。

4 配置说明:
对于常见的CRC完整参数:宽度,多项式,初值,结果异或这四个值照填。
如果Input和Output都需要Reflect,那么选择CRC_DIRCT的LSB模式;
如果两个都不需要Reflect,那么选择MSB模式即可。
只有一个需要Reflect的话,那么结果会错误。(主要是数据序不对,需要再处理一下,不过这种CRC极少。)

5 优化结果:
5-1 Keil MDK和GCC无警告编译通过;GCC C90严格模式编译无警告。
5-2 O2等级优化,ARM926EJ-S Core,ARM指令集,每个字节处理需要10条指令,无论CRC模式。
    O2等级优化,Cortex-M3 Core,Thumb2指令集,每个字节处理需要10条指令,无论CRC模式。每个字节处理约需要15个周期。

这是LSB模式循环的汇编,CortexM3,GCC O2编译:0c:   f810 4b01       ldrb.wr4, , #1
10:   4063            eors    r3, r4
12:   f003 040f       and.w   r4, r3, #15
16:   f852 4024       ldr.w   r4,
1a:   4288            cmp   r0, r1
1c:   ea84 1313       eor.w   r3, r4, r3, lsr #4
20:   f003 040f       and.w   r4, r3, #15
24:   f852 4024       ldr.w   r4,
28:   ea84 1313       eor.w   r3, r4, r3, lsr #4
2c:   d1ee            bne.n   c <crc+0xc>这是MSB模式循环的汇编,CortexM3,GCC O2编译:0c:   f810 4b01       ldrb.wr4, , #1
10:   ea83 6304       eor.w   r3, r3, r4, lsl #24
14:   0f1c            lsrs    r4, r3, #28
16:   f852 4024       ldr.w   r4,
1a:   ea84 1303       eor.w   r3, r4, r3, lsl #4
1e:   0f1c            lsrs    r4, r3, #28
20:   f852 4024       ldr.w   r4,
24:   4288            cmp   r0, r1
26:   ea84 1303       eor.w   r3, r4, r3, lsl #4
2a:   d1ef            bne.n   c <crc+0xc>MDK的结果基本一样,区别在循环终点的判定上,MDK的是用的计数器,GCC是判终止地址。

6 如果有兴趣进一步优化,可以进行循环展开。
如果一次Load 4字节,应该还能获得20%-25%的性能提升,如果数据长度在8字节以上的话(粗略估计)。能节约Load数据,循环比较和跳转的周期数。
另外,Cortex-M3的RevByte之类的指令也很有用,在MSB模式,循环展开的情况下。

7 预处理阶段,基于配置常数进行宏展开,生成复杂的常数表达式。值得注意的是,看来复杂的宏,展开量和展开后的计算量不一定是大的。这和C的算法有区别。
编译阶段常数表达式被编译器计算得到表格的常数。
变量类型和算法根据具体的处理器架构和指令集进行适当变形。
不是太烂的编译器就会输出想要的代码。



dr2001 发表于 2013-1-10 22:02:13

本帖最后由 dr2001 于 2013-1-10 22:15 编辑

最终版本;更新了C代码,以获得最佳优化结果。
该文件展示了这样的内容:配合针对目标体系结构的,优质的编译器,纯C语言生成的代码几乎等价于手工汇编的结果。

在Keil MDK上,O2优化,Cortex-M3,编译得到的结果中,除了Switch/Case没有进行查找表优化外,其他的汇编代码就是我想要的汇编代码。

使用Keil MDK软件仿真功能计算指令执行周期数进行性能分析。(和CortexM3手册对过,结果应该没问题。)
Cortex-M3:
1、不进行循环展开的代码,进行N字节的CRC,需要约:22 + 15 * N周期。
2、进行循环展开,CRC的移位方向和内存的大小端模式都是低位在前或者高位在前,计算CRC的循环开销为:9.75周期/字节。
3、如果CRC和内存大小端是反的,那么CRC循环的开销是:10周期/字节。
A、循环展开受到起始地址是否32Bit对齐;剩余0-3个逐字节访问等影响,额外的开销不定。
B、计算8字节以上CRC,使用循环展开模式就能快了。
C、代码大小:循环展开后256字节左右(含查找表)。

ARM926EJ-S:
循环展开后的核心循环周期数为:12-13周期/字节。和上边的2/3点一致。


以下是CortexM3 MDK编译结果,加了简要注释,CRC是MSB模式;内存布局是小端。            PUSH   {r4-r6,lr}
            LDR      r2, = CRC_INITR
            LDR      r3, = crcLUT ; CRC_LUT_Table
;   If(len <= 4) Jump to Final.
            CMP      r1,#0x04
            BCC      Final

;   Load First Word.
            AND      r4,r0,#0x03
            SUBS   r0,r0,r4
            LSLS   r6,r4,#3
            LDM      r0!,{r5}
            LSRS   r5,r5,r6
            REV      r5,r5
            EORS   r2,r2,r5
            ADDS   r5,r1,r4
            AND      r1,r5,#0x03
            SUBS   r5,r5,r1

;   Swtich/Case Jump.
            CMP      r4,#0x01
            BEQ      Case1
            CMP      r4,#0x02
            BEQ      Case2
            CMP      r4,#0x03
            BNE      Case0
            B      Case3

;   Main Loop.
CRC_UnRoll: LDM      r0!,{r4}
            REV      r4,r4
            EORS   r2,r2,r4
Case0:      LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
            LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
Case1:      LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
            LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
Case2:      LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
            LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
Case3:      LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
            LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
            SUBS   r5,r5,#4
            BNE      CRC_UnRoll
            B      Final

;   Final Bytes.
CRC_Final:LDRB   r4,,#0x01
            EOR      r2,r2,r4,LSL #24
            LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
            LSRS   r4,r2,#28
            LDR      r4,
            EOR      r2,r4,r2,LSL #4
Final:      SUBS   r1,r1,#1
            BCS      CRC_Final

;   Result XOR
            LDR      r0, = CRC_RSXOR
            EORS   r0,r0,r2, LSR (32 - (CRC_WIDTH))
            POP      {r4-r6,pc}不知道现在IAR的编译器结果如何,如果有人能贴一个就更好了。

GCC产生的汇编代码质量略差,数据预处理看起来不是太精简;核心循环里多了一条跳转,主要是对switch/case的结构实现有一个小瑕疵,导致多消耗一个周期。

文件如下:

snoopyzz 发表于 2013-1-6 10:04:27

简单看了下,写的很不错, 只两点
#   define ,个人看不惯把#和define分开....很多编辑器的自动着色也会有问题

另外,看代码,LZ应该会用#error,不知道为啥弄个下面 这种方式让编译器静态检测错误
switch(0) { case 0: case 0: }

dr2001 发表于 2013-1-6 10:12:01

snoopyzz 发表于 2013-1-6 10:04 static/image/common/back.gif
简单看了下,写的很不错, 只两点
#   define ,个人看不惯把#和define分开....很多编辑器的自动着色也会有问 ...

个人爱好#放在第一列,然后预处理命令放在对齐的地方。这样找预处理命令好找。
可能有的编辑器着色因为不支持正则表达会有问题……不过这种编辑器对复杂代码着色估计也不会太好。。。

#error是预处理期的错误,swtich是编译期的错误。两个不是一码事。

反正都符合标准,属于个人习惯而已了。

farmerzhangdl 发表于 2013-1-6 10:18:24

这个真心不错。

dory_m 发表于 2013-1-6 10:18:44

crc 一直没搞懂学习,谢谢!!!{:smile:}

Excellence 发表于 2013-1-6 10:25:44

MARK.
.

Gorgon_Meducer 发表于 2013-1-6 10:40:37

这个要顶一下

dr2001 发表于 2013-1-9 14:17:30

本帖最后由 dr2001 于 2013-1-9 14:24 编辑

更新后的库,新加入了循环展开。

1、原有基于字节Load数据的半字节查表法代码不变。字节的处理顺序为地址增序不变。
2、增加了循环展开的代码,展开次数为4;Load数据基于32Bit进行。
2-1、展开后的代码类似于Cache Line Size = 4Bytes的Cache的行为。即起始地址为3的话,也会读0,1,2的数据;但是尾巴的非对齐部分是逐字节处理的。在内存边界不会有问题。
2-2、由于Load 32Bit数据存在大小端问题,程序进行了相应处理。反序使用了编译器原语或编译器内置函数。
3、尽管代码针对ARM 32Bit架构编写,但是可以运行于其它平台上,就是效率可能会低。无论循环展开与否,代码都可以在ARM/X86/X64上运行,别的平台没测试。
4、由于C无法控制编译器底层行为,循环展开代码只是描述理想情况下的行为;具体优化效果依赖于编译器的行为。MDK可以获得很好的优化效果;GCC-ARM则较差,有冗余代码。
4-1、如果想要最高效的代码,还是用汇编,基本上对着把C翻译成汇编就行了。

大致性能:
1、非展开代码,无论CRC的MSB,LSB模式,MDK开优化的编译结果约为15周期/字节。
2、循环展开,CRC MSB模式在内存小端模式下,MDK输出代码的平均性能为10.8周期/字节。(主要是多了rev指令)
3、循环展开,CRC LSB模式在内存小端模式下,MDK输出代码的平均性能为10.5周期/字节。
-、即CRC模式和内存模式同为MSB/LSB的话,大约是10.5;不同的话,就是10.8,就差一个REV指令的开销。
-、考虑初始地址的全部情况,循环展开后的代码从10字节左右的数据开始速度就会比不展开的代码快;如果编译器不是太差意思,用展开的就行了。

代码体积展开后大约增加一倍多,Thumb2下从112字节总消耗增加为约256字节。(含64B查找表的大小)

代码可以在PC上编译用于确认算法正确性。


haigerl 发表于 2013-1-9 17:12:10

这个可以有!

sept 发表于 2014-3-15 03:16:59

dr2001 发表于 2013-1-9 14:17
更新后的库,新加入了循环展开。

1、原有基于字节Load数据的半字节查表法代码不变。字节的处理顺序为地址 ...

请教大神 代码可以对8位字符数组进行crc16校验并返回16位的结果吗 define处应该怎么选择

yanyi103 发表于 2014-3-19 13:47:46

这个可以有,收下了

huangxiaowei 发表于 2014-4-16 14:28:35

   顶一下

RAMILE 发表于 2014-10-24 16:13:17

今天用到dr2001的CRC万能库,modbus里面需要如下配置
/*--         CRC Algorithm Information      --*/
/*CRC Width.          <Integer, >      */
#define CRC_WIDTH               16

/*CRC Shift Direction. <LSB | MSB>            */
#define CRC_DIRCT               LSB

/*CRC polynomial.   <Integer, end with ul>*/
#define CRC_POLY               0x8005ul// 0xa001

/*CRC Reg Init Value. <Integer, end with ul>*/
#define CRC_INIT                0xFFFFul

/*CRC XOR Ret Value.<Integer, end with ul>*/
#define CRC_XORV                0x0000ul

/*--    UnRoll CRC Calculation Loop Options   --*/
/*If Unroll Loop is Enabled. (Suggested ON.)*\
\*                      <optEnable | optDisable>*/
#define CRC_UNROLL_LOOP         optEnable

/*Memory Mode, used by UnRoll.                *\
\*<LSB | MSB> First / At Lower Address.       */
#define CRC_MEMORY_MODE         LSB

icoyool 发表于 2021-8-6 21:24:37

后排膜拜VIP+++大神的神作!

icoyool 发表于 2021-8-12 09:58:18

楼主你好, 使用你的库有个疑问, 如果分段计算传入初值会有点麻烦,
另外switch里面嵌套一个while,确实第一次见,长见识了

      switch(tmp) {
            while(1) {
                default:
                case 0: dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                case 1: dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                case 2: dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                case 3: dat =   crcLUT;
                crc =   dat ^ (crc >>4);
                dat =   crcLUT;
                crc =   dat ^ (crc >>4);
               
                cur ++;
               
                if(len < 4) {
                  break;
                }
               
                len -=4;
                dat =   *cur;
#               if(CRC_MEMORY_MODE == MSB)
                CRC_INTRIN_REV(dat);
#               endif
                crc =   crc ^ dat;
            }
      }

zcllom 发表于 2021-8-12 10:07:15

icoyool 发表于 2021-8-12 09:58
楼主你好, 使用你的库有个疑问, 如果分段计算传入初值会有点麻烦,
另外switch里面嵌套一个while,确实第一 ...

switch在这里用的出神入化,怎么会有这样的奇思妙想呢?

zcllom 发表于 2021-8-13 17:27:37

zcllom 发表于 2021-8-12 10:07
switch在这里用的出神入化,怎么会有这样的奇思妙想呢?

类似的用法还有:实现一个字符串的拷贝

voidcopy( char *dst,char *src,intlen)
{
    switch(len & 7)
    {
      default:
      while (len > 7)
      {
            len -= 8;
            *dst++ = *src++;
   
            case 7:*dst++ = *src++;
   
            case 6:*dst++ = *src++;
   
            case 5:*dst++ = *src++;
   
            case 4:*dst++ = *src++;
   
            case 3:*dst++ = *src++;
   
            case 2:*dst++ = *src++;
   
            case 1:*dst++ = *src++;
      
      }
   
    }

}
页: [1]
查看完整版本: [代码][C库]针对32Bit ARM优化的CRC库,静态配置支持大部分CRC。