【问题标题】:CRC32 Calculation for Zero Filled Buffer/File零填充缓冲区/文件的 CRC32 计算
【发布时间】:2019-03-31 18:45:57
【问题描述】:

如果我想计算大量连续零字节的 CRC32 值,在给定零运行的长度的情况下,我可以使用一个恒定时间公式吗?例如,如果我知道我有 1000 个字节全部用零填充,有没有办法避免 1000 次迭代的循环(只是一个例子,为了这个问题,实际的零数量是无限的)?

【问题讨论】:

  • 是的,有。你知道 GF(2) 上的多项式是如何工作的吗?
  • 在 zlib 源代码中的 Mark Adler 的 crc32_combine 中描述了一种排序日志(零数)的方法。它可以推广到其他CRC算法。
  • @rcgldr for n 零字节,CRC 是 initial_value*(x^8n) mod poly。您可以通过平方来计算 x^8n mod polyen.wikipedia.org/wiki/Exponentiation_by_squaring ... 但如果我说当他不知道这意味着什么时,它不会对 OP 有任何好处。
  • @MattTimmermans - 我删除了我之前的评论。 OP 要求一个常数时间“公式”,如果 n 是一个常数,这是可能的。
  • @rcgldr, n 不必是常数。它只需要有界。如果 n > 2^32,那么你可以减少它 mod 2^32-1,因为 n 零的 CRC 模式重复那个时期。在通常的假设下,您可以在恒定时间内对 n 进行算术运算,那么,平方取幂最多需要 32 步,即可以在恒定时间内完成。好的,在这种情况下,这个假设有点开玩笑,但对于真正的实际目的,它是常数时间,除非函数将 n 作为大数。如果 n 是一个大数,那么它就是 O(log n) 仅用于模数。

标签: algorithm crc crc32


【解决方案1】:

使用查表和乘法可以将时间复杂度降低到 O(1)。解释和示例代码显示在此答案的第三部分。

如果 1000 是一个常数,则为一个由 32 个值组成的预计算表,每个值代表 可以使用 CRC 到 8000 次方 mod poly 的每一位。一组矩阵(每个 CRC 字节一组)可用于一次处理一个字节。两种方法都是恒定时间(固定循环数)O(1)。

如上所述,如果 1000 不是常数,则可以使用平方取幂,这将是 O(log2(n)) 时间复杂度,或者针对某个恒定数量的零位的预先计算表的组合,例如为 256,然后可以使用平方取幂,这样最后一步就是 O(log2(n%256))。


一般优化:对于具有零和非零元素的普通数据,在带有 pclmulqdq(使用 xmm 寄存器)的现代 X86 上,可以实现快速的 crc32(或 crc16),尽管它接近 500 行汇编代码.英特尔文档:crc using pclmulqdqgithub fast crc16 的示例源代码。对于 32 位 CRC,需要一组不同的常数。如果有兴趣,我将源代码转换为使用 Visual Studio ML64.EXE(64 位 MASM),并为左移和右移 32 位 CRC 创建了示例,每个示例都有两组常量,用于两个最流行的 CRC 32 位多项式(左移多边形:crc32:0x104C11DB7 和 crc32c:0x11EDC6F41,右移多边形位反转)。


使用基于软件的无进位乘法模 CRC 多项式快速调整 CRC 的示例代码。这将比使用 32 x 32 矩阵乘法快得多。计算非零数据的 CRC:crf = GenCrc(msg, ...)。为 n 个零字节计算调整常数: pmc = pow(2^(8*n))%poly (使用重复平方的取幂)。然后针对零字节调整CRC:crf = (crf*pmc)%poly。

请注意,时间复杂度可以通过为 i = 1 到 n 生成 pow(2^(8*i))%poly 常量表而降低到 O(1)。然后计算是查表和固定迭代(32个周期)乘以% poly。

#include <stdio.h>
#include <stdlib.h>

typedef unsigned char       uint8_t;
typedef unsigned int       uint32_t;

static uint32_t crctbl[256];

void GenTbl(void)                       /* generate crc table */
{
uint32_t crc;
uint32_t c;
uint32_t i;
    for(c = 0; c < 0x100; c++){
        crc = c<<24;
        for(i = 0; i < 8; i++)
            crc = (crc<<1)^((0-(crc>>31))&0x04c11db7);
        crctbl[c] = crc;
    }
}

uint32_t GenCrc(uint8_t * bfr, size_t size) /* generate crc */
{
uint32_t crc = 0u;
    while(size--)
        crc = (crc<<8)^crctbl[(crc>>24)^*bfr++];
    return(crc);
}

/* carryless multiply modulo crc */
uint32_t MpyModCrc(uint32_t a, uint32_t b) /* (a*b)%crc */
{
uint32_t pd = 0;
uint32_t i;
    for(i = 0; i < 32; i++){
        pd = (pd<<1)^((0-(pd>>31))&0x04c11db7u);
        pd ^= (0-(b>>31))&a;
        b <<= 1;
    }
    return pd;
}

/* exponentiate by repeated squaring modulo crc */
uint32_t PowModCrc(uint32_t p)          /* pow(2,p)%crc */
{
uint32_t prd = 0x1u;                    /* current product */
uint32_t sqr = 0x2u;                    /* current square */
    while(p){
        if(p&1)
            prd = MpyModCrc(prd, sqr);
        sqr = MpyModCrc(sqr, sqr);
        p >>= 1;
    }
    return prd;
}

/* # data bytes */
#define DAT  ( 32)
/* # zero bytes */
#define PAD  (992)
/* DATA+PAD */
#define CNT (1024)

int main()
{
uint32_t pmc;
uint32_t crc;
uint32_t crf;
uint32_t i;
uint8_t *msg = malloc(CNT);

    for(i = 0; i < DAT; i++)           /* generate msg */
        msg[i] = (uint8_t)rand();
    for( ; i < CNT; i++)
        msg[i] = 0;
    GenTbl();                           /* generate crc table */
    crc = GenCrc(msg, CNT);             /* generate crc normally */
    crf = GenCrc(msg, DAT);             /* generate crc for data */
    pmc = PowModCrc(PAD*8);             /* pmc = pow(2,PAD*8)%crc */
    crf = MpyModCrc(crf, pmc);          /* crf = (crf*pmc)%crc */
    printf("%08x %08x\n", crc, crf);    /* crf == crc */
    free(msg);
    return 0;
}

【讨论】:

  • 在现代 CPU 上,快速 crc32(以及 8、16 和 64)已经在硬件中实现。正好1行汇编代码:software.intel.com/sites/landingpage/IntrinsicsGuide/…
  • @Sonts - 该指令仅适用于右移 crc32c(特定多项式)。
  • 对于操作来说可能已经足够好了。指令非常快,1000 个值 = 不到 1 微秒,几乎无法测量。
  • @Sonts - 如果 OP 使用不同的多项式,或者使用左移 CRC,那么该指令将无济于事。即使 OP 使用的是右移 crc32c,在我的系统 Intel 3770K 3.5 ghz 上,对于 256 MB,pclmulqdq => 0.0783919 秒,crc32 内在 => 0.0541801 秒,速度也没有那么快。虽然它是 500 行代码而不是 1 行代码。
【解决方案2】:

您可以计算应用 n 个零的结果,而不是在 O(1) 时间内,而是在 O(log n) 时间内。这是在 zlib 的crc32_combine() 中完成的。构造一个二进制矩阵,表示将单个零位应用于 CRC 的操作。 32x32 矩阵将 32 位 CRC 与 GF(2) 相乘,其中加法由异或 (^) 代替,乘法由和 (&) 逐位代替。

然后可以对该矩阵进行平方以获得两个零的运算符。平方得到四个零的运算符。第三个平方得到八个零的运算符。根据需要等等。

现在可以根据要计算其 CRC 的 n 个零位中的一位,将一组运算符应用于 CRC。

如果您碰巧知道您将经常准确地应用那么多零,您可以预先计算任意数量的零位的结果矩阵运算符。那么它只是一个矩阵乘以一个向量,实际上是O(1)。

您不需要使用此处另一个答案中建议的pclmulqdq 指令,但如果您拥有它会更快一些。它不会改变操作的 O()。

【讨论】:

  • 在我的回答中提到 pclmulqdq 是针对具有非零数据的快速 crc32。我更新了我的答案以明确这一点。
  • 我更新了我的答案,注意使用表格查找和基于软件的无进位乘法模 crc 多项式可以实现 O(1) 的时间复杂度。
【解决方案3】:

CRC32 基于 GF(2)[X] 中的乘法模数多项式,它是乘法的。棘手的部分是将非乘法与乘法分开。

首先定义一个具有以下结构的稀疏文件(在 Go 中):

type SparseFile struct {
    FileBytes []SparseByte
    Size      uint64
}
type SparseByte struct {
    Position uint64
    Value    byte
}

在你的情况下是SparseFile{[]FileByte{}, 1000}

那么,函数将是:

func IEEESparse (file SparseFile) uint32 {
    position2Index := map[uint64]int{}
    for i , v := range(file.FileBytes) {
        file.FileBytes[i].Value = bits.Reverse8(v.Value)
        position2Index[v.Position] = i
    }
    for i := 0; i < 4; i++ {
        index, ok := position2Index[uint64(i)]
        if !ok {
            file.FileBytes = append(file.FileBytes, SparseByte{Position: uint64(i), Value: 0xFF})
        } else {
            file.FileBytes[index].Value ^= 0xFF
        }
    }

    // Add padding
    file.Size += 4
    newReminder := bits.Reverse32(reminderIEEESparse(file))

    return newReminder ^ 0xFFFFFFFF
}

所以请注意:

  1. 以相反的顺序(每个字节)对位执行除法。
  2. 前四个字节与 0xFF 异或。
  3. 文件用 4 个字节填充。
  4. 提醒再次反转。
  5. 提醒再次异或。

内部函数reminderIEEESparse是真正的提醒,在O(log n)中很容易实现,其中n是文件的大小。

您可以找到完整的实现here

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-12
    • 2023-02-10
    • 2011-02-20
    • 1970-01-01
    • 1970-01-01
    • 2013-02-21
    相关资源
    最近更新 更多