NAND 纠错码

简介

在研究了 linux mtd/nand Hamming 软件 ECC 引擎驱动程序之后,我感觉还有优化的空间。我花了几个小时修改代码,执行了诸如查表、删除多余代码等技巧。之后,速度提高了 35-40%。但我仍然不太满意,因为我觉得还有改进的余地。

糟糕!我被迷住了。我决定在这个文件中注释我的步骤。也许这对某人有用,或者某人从中学习到一些东西。

问题

NAND 闪存(至少是 SLC 闪存)通常具有 256 字节的扇区。然而,NAND 闪存并非极其可靠,因此需要一些错误检测(有时是纠正)。

这是通过汉明码实现的。我将尝试用通俗易懂的语言来解释它(如果我没有使用正确的术语,请向该领域的所有专业人士道歉,我的编码理论课几乎是 30 年前的事情了,我必须承认它不是我最喜欢的课程)。

正如我之前所说,ecc 计算是在 256 字节的扇区上执行的。这是通过计算行和列上的几个奇偶校验位来完成的。使用的奇偶校验是偶校验,这意味着如果计算奇偶校验的数据为 1,则奇偶校验位 = 1,如果计算奇偶校验的数据为 0,则奇偶校验位 = 0。因此,计算奇偶校验的数据上的总位数 + 奇偶校验位是偶数。(如果你不明白,请参考维基百科)。奇偶校验通常通过异或运算来计算,有时也称为 xor。在 C 中,xor 的运算符是 ^

回到 ecc。我们给出一个小图

字节 0

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

rp0

rp2

rp4

...

rp14

字节 1

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

rp1

rp2

rp4

...

rp14

字节 2

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

rp0

rp3

rp4

...

rp14

字节 3

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

rp1

rp3

rp4

...

rp14

字节 4

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

rp0

rp2

rp5

...

rp14

...

字节 254

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

rp0

rp3

rp5

...

rp15

字节 255

位 7 cp1 cp3 cp5

位 6 cp0 cp3 cp5

位 5 cp1 cp2 cp5

位 4 cp0 cp2 cp5

位 3 cp1 cp3 cp4

位 2 cp0 cp3 cp4

位 1 cp1 cp2 cp4

位 0 cp0 cp2 cp4

rp1

rp3

rp5

...

rp15

此图表示 256 字节的扇区。cp 是我列奇偶校验的缩写,rp 是行奇偶校验的缩写。

让我们开始解释列奇偶校验。

  • cp0 是属于所有位 0、位 2、位 4、位 6 的奇偶校验。

    因此,所有位 0、位 2、位 4 和位 6 的值 + cp0 本身的总和是偶数。

类似地,cp1 是所有位 1、位 3、位 5 和位 7 的总和。

  • cp2 是位 0、位 1、位 4 和位 5 的奇偶校验

  • cp3 是位 2、位 3、位 6 和位 7 的奇偶校验。

  • cp4 是位 0、位 1、位 2 和位 3 的奇偶校验。

  • cp5 是位 4、位 5、位 6 和位 7 的奇偶校验。

请注意,cp0 .. cp5 中的每一个都正好是一位。

行奇偶校验实际上工作方式几乎相同。

  • rp0 是所有偶数字节 (0, 2, 4, 6, ... 252, 254) 的奇偶校验

  • rp1 是所有奇数字节 (1, 3, 5, 7, ..., 253, 255) 的奇偶校验

  • rp2 是所有字节 0、1、4、5、8、9、... 的奇偶校验(因此处理两个字节,然后跳过 2 个字节)。

  • rp3 涵盖 rp2 未涵盖的一半(字节 2、3、6、7、10、11、...)

  • 对于 rp4,规则是覆盖 4 个字节,跳过 4 个字节,覆盖 4 个字节,跳过 4 个字节等。

    因此,rp4 计算字节 0、1、2、3、8、9、10、11、16、... 的奇偶校验)

  • rp5 覆盖另一半,因此是字节 4、5、6、7、12、13、14、15、20、..

现在的故事变得相当无聊。我想你明白了。

  • rp6 覆盖 8 个字节,然后跳过 8 个字节等

  • rp7 跳过 8 个字节,然后覆盖 8 个字节等

  • rp8 覆盖 16 个字节,然后跳过 16 个字节等

  • rp9 跳过 16 个字节,然后覆盖 16 个字节等

  • rp10 覆盖 32 个字节,然后跳过 32 个字节等

  • rp11 跳过 32 个字节,然后覆盖 32 个字节等

  • rp12 覆盖 64 个字节,然后跳过 64 个字节等

  • rp13 跳过 64 个字节,然后覆盖 64 个字节等

  • rp14 覆盖 128 个字节,然后跳过 128 个字节

  • rp15 跳过 128 个字节,然后覆盖 128 个字节

最后,奇偶校验位按如下方式组合成三个字节

ECC

位 7

位 6

位 5

位 4

位 3

位 2

位 1

位 0

ECC 0

rp07

rp06

rp05

rp04

rp03

rp02

rp01

rp00

ECC 1

rp15

rp14

rp13

rp12

rp11

rp10

rp09

rp08

ECC 2

cp5

cp4

cp3

cp2

cp1

cp0

1

1

在写完这个之后,我发现 ST 应用笔记 AN1823 (http://www.st.com/stonline/) 给出了一个更好的图。(但他们使用线奇偶校验作为术语,而我使用行奇偶校验)好吧,我的图形能力有待提高,所以请忍受我一下 :-)

而且由于版权原因,我也无法重用 ST 的图片。

尝试 0

实现奇偶校验计算非常简单。在 C 伪代码中

for (i = 0; i < 256; i++)
{
  if (i & 0x01)
     rp1 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp1;
  else
     rp0 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp0;
  if (i & 0x02)
     rp3 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp3;
  else
     rp2 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp2;
  if (i & 0x04)
    rp5 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp5;
  else
    rp4 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp4;
  if (i & 0x08)
    rp7 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp7;
  else
    rp6 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp6;
  if (i & 0x10)
    rp9 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp9;
  else
    rp8 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp8;
  if (i & 0x20)
    rp11 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp11;
  else
    rp10 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp10;
  if (i & 0x40)
    rp13 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp13;
  else
    rp12 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp12;
  if (i & 0x80)
    rp15 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp15;
  else
    rp14 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ bit3 ^ bit2 ^ bit1 ^ bit0 ^ rp14;
  cp0 = bit6 ^ bit4 ^ bit2 ^ bit0 ^ cp0;
  cp1 = bit7 ^ bit5 ^ bit3 ^ bit1 ^ cp1;
  cp2 = bit5 ^ bit4 ^ bit1 ^ bit0 ^ cp2;
  cp3 = bit7 ^ bit6 ^ bit3 ^ bit2 ^ cp3
  cp4 = bit3 ^ bit2 ^ bit1 ^ bit0 ^ cp4
  cp5 = bit7 ^ bit6 ^ bit5 ^ bit4 ^ cp5
}

分析 0

C 确实具有按位运算符,但没有真正有效地执行上述操作的运算符(大多数硬件也没有这样的指令)。因此,如果不实现这一点,很明显上面的代码不会给我带来诺贝尔奖 :-)

幸运的是,异或运算是可交换的,因此我们可以按任意顺序组合这些值。因此,与其单独计算所有位,不如尝试重新排列。对于列奇偶校验,这很容易。我们可以直接异或这些字节,最后过滤掉相关的位。这非常好,因为它会将所有 cp 计算移出 for 循环。

类似地,我们可以先异或各个行的字节。这导致

尝试 1

const char parity[256] = {
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0
};

void ecc1(const unsigned char *buf, unsigned char *code)
{
    int i;
    const unsigned char *bp = buf;
    unsigned char cur;
    unsigned char rp0, rp1, rp2, rp3, rp4, rp5, rp6, rp7;
    unsigned char rp8, rp9, rp10, rp11, rp12, rp13, rp14, rp15;
    unsigned char par;

    par = 0;
    rp0 = 0; rp1 = 0; rp2 = 0; rp3 = 0;
    rp4 = 0; rp5 = 0; rp6 = 0; rp7 = 0;
    rp8 = 0; rp9 = 0; rp10 = 0; rp11 = 0;
    rp12 = 0; rp13 = 0; rp14 = 0; rp15 = 0;

    for (i = 0; i < 256; i++)
    {
        cur = *bp++;
        par ^= cur;
        if (i & 0x01) rp1 ^= cur; else rp0 ^= cur;
        if (i & 0x02) rp3 ^= cur; else rp2 ^= cur;
        if (i & 0x04) rp5 ^= cur; else rp4 ^= cur;
        if (i & 0x08) rp7 ^= cur; else rp6 ^= cur;
        if (i & 0x10) rp9 ^= cur; else rp8 ^= cur;
        if (i & 0x20) rp11 ^= cur; else rp10 ^= cur;
        if (i & 0x40) rp13 ^= cur; else rp12 ^= cur;
        if (i & 0x80) rp15 ^= cur; else rp14 ^= cur;
    }
    code[0] =
        (parity[rp7] << 7) |
        (parity[rp6] << 6) |
        (parity[rp5] << 5) |
        (parity[rp4] << 4) |
        (parity[rp3] << 3) |
        (parity[rp2] << 2) |
        (parity[rp1] << 1) |
        (parity[rp0]);
    code[1] =
        (parity[rp15] << 7) |
        (parity[rp14] << 6) |
        (parity[rp13] << 5) |
        (parity[rp12] << 4) |
        (parity[rp11] << 3) |
        (parity[rp10] << 2) |
        (parity[rp9]  << 1) |
        (parity[rp8]);
    code[2] =
        (parity[par & 0xf0] << 7) |
        (parity[par & 0x0f] << 6) |
        (parity[par & 0xcc] << 5) |
        (parity[par & 0x33] << 4) |
        (parity[par & 0xaa] << 3) |
        (parity[par & 0x55] << 2);
    code[0] = ~code[0];
    code[1] = ~code[1];
    code[2] = ~code[2];
}

仍然非常简单。最后三个反转语句是为了给空闪存提供 0xff 0xff 0xff 的校验和。在空闪存中,所有数据都是 0xff,因此校验和会匹配。

我还引入了奇偶校验查找。我希望这是计算奇偶校验的最快方法,但我稍后将研究替代方法。

分析 1

代码可以工作,但效率不高。在我的系统上,它花费的时间几乎是 Linux 驱动程序代码的 4 倍。但是,嘿,如果那么容易,这早就完成了。没有痛苦,就没有收获。

幸运的是,还有很大的改进空间。

在第 1 步中,我们从按位计算转移到按字节计算。然而,在 C 中,我们也可以使用无符号 long 数据类型,而且几乎每个现代微处理器都支持 32 位操作,所以为什么不尝试以这种方式编写我们的代码,以便我们以 32 位块处理数据。

当然,这意味着需要进行一些修改,因为行奇偶校验是逐字节进行的。快速分析:对于列奇偶校验,我们使用 par 变量。当扩展到 32 位时,我们可以很容易地从中计算出 rp0 和 rp1。(因为 par 现在由 4 个字节组成,分别从 MSB 到 LSB 贡献给 rp1、rp0、rp1、rp0)此外,rp2 和 rp3 也可以很容易地从 par 中检索,因为 rp3 覆盖了前两个 MSB,而 rp2 覆盖了最后两个 LSB。

请注意,现在循环只执行 64 次 (256/4)。 并且注意字节顺序。 字节在 long 中的顺序是机器相关的,可能会影响我们。无论如何,如果出现问题:此代码是在 x86 上开发的(准确地说:一台带有 D920 Intel CPU 的 DELL PC)。

当然,性能可能取决于对齐方式,但我预计 nand 驱动程序中的 I/O 缓冲区会正确对齐(否则应修复以获得最大性能)。

让我们试试看...

尝试 2

extern const char parity[256];

void ecc2(const unsigned char *buf, unsigned char *code)
{
    int i;
    const unsigned long *bp = (unsigned long *)buf;
    unsigned long cur;
    unsigned long rp0, rp1, rp2, rp3, rp4, rp5, rp6, rp7;
    unsigned long rp8, rp9, rp10, rp11, rp12, rp13, rp14, rp15;
    unsigned long par;

    par = 0;
    rp0 = 0; rp1 = 0; rp2 = 0; rp3 = 0;
    rp4 = 0; rp5 = 0; rp6 = 0; rp7 = 0;
    rp8 = 0; rp9 = 0; rp10 = 0; rp11 = 0;
    rp12 = 0; rp13 = 0; rp14 = 0; rp15 = 0;

    for (i = 0; i < 64; i++)
    {
        cur = *bp++;
        par ^= cur;
        if (i & 0x01) rp5 ^= cur; else rp4 ^= cur;
        if (i & 0x02) rp7 ^= cur; else rp6 ^= cur;
        if (i & 0x04) rp9 ^= cur; else rp8 ^= cur;
        if (i & 0x08) rp11 ^= cur; else rp10 ^= cur;
        if (i & 0x10) rp13 ^= cur; else rp12 ^= cur;
        if (i & 0x20) rp15 ^= cur; else rp14 ^= cur;
    }
    /*
       we need to adapt the code generation for the fact that rp vars are now
       long; also the column parity calculation needs to be changed.
       we'll bring rp4 to 15 back to single byte entities by shifting and
       xoring
    */
    rp4 ^= (rp4 >> 16); rp4 ^= (rp4 >> 8); rp4 &= 0xff;
    rp5 ^= (rp5 >> 16); rp5 ^= (rp5 >> 8); rp5 &= 0xff;
    rp6 ^= (rp6 >> 16); rp6 ^= (rp6 >> 8); rp6 &= 0xff;
    rp7 ^= (rp7 >> 16); rp7 ^= (rp7 >> 8); rp7 &= 0xff;
    rp8 ^= (rp8 >> 16); rp8 ^= (rp8 >> 8); rp8 &= 0xff;
    rp9 ^= (rp9 >> 16); rp9 ^= (rp9 >> 8); rp9 &= 0xff;
    rp10 ^= (rp10 >> 16); rp10 ^= (rp10 >> 8); rp10 &= 0xff;
    rp11 ^= (rp11 >> 16); rp11 ^= (rp11 >> 8); rp11 &= 0xff;
    rp12 ^= (rp12 >> 16); rp12 ^= (rp12 >> 8); rp12 &= 0xff;
    rp13 ^= (rp13 >> 16); rp13 ^= (rp13 >> 8); rp13 &= 0xff;
    rp14 ^= (rp14 >> 16); rp14 ^= (rp14 >> 8); rp14 &= 0xff;
    rp15 ^= (rp15 >> 16); rp15 ^= (rp15 >> 8); rp15 &= 0xff;
    rp3 = (par >> 16); rp3 ^= (rp3 >> 8); rp3 &= 0xff;
    rp2 = par & 0xffff; rp2 ^= (rp2 >> 8); rp2 &= 0xff;
    par ^= (par >> 16);
    rp1 = (par >> 8); rp1 &= 0xff;
    rp0 = (par & 0xff);
    par ^= (par >> 8); par &= 0xff;

    code[0] =
        (parity[rp7] << 7) |
        (parity[rp6] << 6) |
        (parity[rp5] << 5) |
        (parity[rp4] << 4) |
        (parity[rp3] << 3) |
        (parity[rp2] << 2) |
        (parity[rp1] << 1) |
        (parity[rp0]);
    code[1] =
        (parity[rp15] << 7) |
        (parity[rp14] << 6) |
        (parity[rp13] << 5) |
        (parity[rp12] << 4) |
        (parity[rp11] << 3) |
        (parity[rp10] << 2) |
        (parity[rp9]  << 1) |
        (parity[rp8]);
    code[2] =
        (parity[par & 0xf0] << 7) |
        (parity[par & 0x0f] << 6) |
        (parity[par & 0xcc] << 5) |
        (parity[par & 0x33] << 4) |
        (parity[par & 0xaa] << 3) |
        (parity[par & 0x55] << 2);
    code[0] = ~code[0];
    code[1] = ~code[1];
    code[2] = ~code[2];
}

奇偶校验数组不再显示。 另请注意,对于这些示例,我有点偏离了我通常的编程风格,允许在一行上使用多个语句,在只有一个语句的 then 和 else 块中不使用 { },并使用 ^= 等运算符。

分析 2

该代码(当然)有效,并且欢呼:我们比 Linux 驱动程序代码快一点(大约 15%)。但是等等,别高兴得太早。还有更多可以获得的。如果我们查看例如 rp14 和 rp15,我们看到我们要么用 rp14 要么用 rp15 对我们的数据进行异或运算。然而,我们也有 par,它遍历所有数据。这意味着不需要计算 rp14,因为可以从 rp15 通过 rp14 = par ^ rp15 计算得出,因为 par = rp14 ^ rp15; (或者如果需要,我们可以避免计算 rp15 并从 rp14 计算它)。这就是为什么有些地方提到反向奇偶校验。当然,同样的事情也适用于 rp4/5、rp6/7、rp8/9、rp10/11 和 rp12/13。实际上,这意味着我们可以从 if 语句中消除 else 子句。此外,我们可以通过首先从 long 到 byte 来稍微优化最后的计算。实际上我们甚至可以避免查表

尝试 3

奇数被替换

if (i & 0x01) rp5 ^= cur; else rp4 ^= cur;
if (i & 0x02) rp7 ^= cur; else rp6 ^= cur;
if (i & 0x04) rp9 ^= cur; else rp8 ^= cur;
if (i & 0x08) rp11 ^= cur; else rp10 ^= cur;
if (i & 0x10) rp13 ^= cur; else rp12 ^= cur;
if (i & 0x20) rp15 ^= cur; else rp14 ^= cur;

if (i & 0x01) rp5 ^= cur;
if (i & 0x02) rp7 ^= cur;
if (i & 0x04) rp9 ^= cur;
if (i & 0x08) rp11 ^= cur;
if (i & 0x10) rp13 ^= cur;
if (i & 0x20) rp15 ^= cur;

并在循环外添加

rp4  = par ^ rp5;
rp6  = par ^ rp7;
rp8  = par ^ rp9;
rp10  = par ^ rp11;
rp12  = par ^ rp13;
rp14  = par ^ rp15;

之后,代码花费的时间增加了大约 30%,尽管语句的数量减少了。这也反映在汇编代码中。

分析 3

非常奇怪。猜想这与缓存或指令并行性有关。我也在 eeePC(赛扬,主频 900 Mhz)上试过。有趣的观察结果是,这个运行代码比我的 3Ghz D920 处理器慢 30%(根据时间)。

嗯,预计这不容易,所以也许可以转移到不同的轨道:让我们回到 attempt2 中的代码并进行一些循环展开。这将消除一些 if 语句。我将尝试不同的展开量,看看哪个效果最好。

尝试 4

将循环展开 1、2、3 和 4 次。对于 4,代码以

for (i = 0; i < 4; i++)
{
    cur = *bp++;
    par ^= cur;
    rp4 ^= cur;
    rp6 ^= cur;
    rp8 ^= cur;
    rp10 ^= cur;
    if (i & 0x1) rp13 ^= cur; else rp12 ^= cur;
    if (i & 0x2) rp15 ^= cur; else rp14 ^= cur;
    cur = *bp++;
    par ^= cur;
    rp5 ^= cur;
    rp6 ^= cur;
    ...

分析 4

展开一次大约可获得 15% 的增益

展开两次使增益保持在 15% 左右

展开三次使增益比尝试 2 提高了 30%。

展开四次与展开三次相比,改进幅度很小。

我决定无论如何都继续进行四次展开循环。我的直觉是,在接下来的步骤中,我将从中获得额外的收益。

下一步是由 par 包含所有字节的异或以及 rp4 和 rp5 各自包含一半字节的异或这一事实触发的。所以实际上 par = rp4 ^ rp5。但是,由于异或运算是可交换的,我们也可以说 rp5 = par ^ rp4。因此,不需要保留 rp4 和 rp5。我们可以消除 rp5(或 rp4,但我已经预见到另一种优化)。同样适用于 rp6/7、rp8/9、rp10/11 rp12/13 和 rp14/15。

尝试 5

实际上,循环中所有奇数位的 rp 赋值都被删除了。这包括 if 语句的 else 子句。当然,在循环之后,我们需要通过添加诸如

rp5 = par ^ rp4;

此外,可以删除初始赋值 (rp5 = 0; 等)。在此过程中,我还删除了 rp0/1/2/3 的初始化。

分析 5

测量表明这是一个好举动。与尝试 4 的 4 次展开相比,运行时间大致减半,并且我们只需要 Linux 内核中当前代码 1/3 的处理器时间。

然而,我仍然认为还有更多。我不喜欢所有 if 语句。为什么不保留一个正在运行的奇偶校验并只保留最后一个 if 语句呢。是时候进行另一个版本了!

尝试 6

for 循环内的代码更改为

for (i = 0; i < 4; i++)
{
    cur = *bp++; tmppar  = cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= tmppar;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur; rp8 ^= tmppar;

    cur = *bp++; tmppar ^= cur; rp4 ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur; rp10 ^= tmppar;

    cur = *bp++; tmppar ^= cur; rp4 ^= cur; rp6 ^= cur; rp8 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= cur; rp8 ^= cur;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur; rp8 ^= cur;
    cur = *bp++; tmppar ^= cur; rp8 ^= cur;

    cur = *bp++; tmppar ^= cur; rp4 ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur;

    par ^= tmppar;
    if ((i & 0x1) == 0) rp12 ^= tmppar;
    if ((i & 0x2) == 0) rp14 ^= tmppar;
}

正如您所看到的,tmppar 用于累积 for 迭代中的奇偶校验。在最后 3 个语句中,它被添加到 par,并且如果需要,则添加到 rp12 和 rp14。

在进行更改时,我还发现我可以利用 tmppar 包含此迭代的运行奇偶校验。因此,我没有使用:rp4 ^= cur; rp6 ^= cur; 我删除了 rp6 ^= cur; 语句,并在下一个语句中执行 rp6 ^= tmppar;。对 rp8 和 rp10 进行了类似的更改

分析 6

再次测量此代码显示出很大的增益。当执行原始的 Linux 代码 100 万次时,在我的系统上大约需要 1 秒。(使用 time 来测量性能)。在此迭代之后,我回到了 0.075 秒。实际上,我必须决定开始测量超过 1000 万次迭代,以便不会损失太多精度。这一个绝对似乎是头奖!

不过,还有一些改进的余地。有三个地方有语句

rp4 ^= cur; rp6 ^= cur;

在 while 循环中也维护一个变量 rp4_6 似乎更有效;这消除了每个循环 3 个语句。当然,在循环之后,我们需要通过添加

rp4 ^= rp4_6;
rp6 ^= rp4_6

此外,对 rp8 有 4 个连续赋值。可以通过在这些 4 行之前保存 tmppar,然后执行 rp8 = rp8 ^ tmppar ^ notrp8; (其中 notrp8 是这 4 行之前 rp8 的值)来更有效地对此进行编码。再次使用异或的交换性质。是时候进行新的测试了!

尝试 7

新代码现在看起来像

for (i = 0; i < 4; i++)
{
    cur = *bp++; tmppar  = cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= tmppar;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur; rp8 ^= tmppar;

    cur = *bp++; tmppar ^= cur; rp4_6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur; rp10 ^= tmppar;

    notrp8 = tmppar;
    cur = *bp++; tmppar ^= cur; rp4_6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur;
    rp8 = rp8 ^ tmppar ^ notrp8;

    cur = *bp++; tmppar ^= cur; rp4_6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp6 ^= cur;
    cur = *bp++; tmppar ^= cur; rp4 ^= cur;
    cur = *bp++; tmppar ^= cur;

    par ^= tmppar;
    if ((i & 0x1) == 0) rp12 ^= tmppar;
    if ((i & 0x2) == 0) rp14 ^= tmppar;
}
rp4 ^= rp4_6;
rp6 ^= rp4_6;

不是很大的变化,但每一分钱都很重要 :-)

分析 7

实际上,这使事情变得更糟。不是很多,但我不想朝着错误的方向前进。也许以后要调查一下。可能与缓存再次有关。

猜想这就是在循环内可以赢得的。也许再展开一次会有所帮助。我暂时保留 7 中的优化。

尝试 8

将循环再展开一次。

分析 8

这使事情变得更糟。让我们坚持使用尝试 6 并从那里继续。尽管循环内的代码似乎无法进一步优化,但仍有优化的空间来优化 ecc 代码的生成。我们可以简单地计算总奇偶校验。如果它是 0,则 rp4 = rp5 等。如果奇偶校验为 1,则 rp4 = !rp5;

但是,如果 rp4 = rp5,我们不需要 rp5 等。我们只需将偶数位写入结果字节,然后执行类似的操作

code[0] |= (code[0] << 1);

让我们测试一下。

尝试 9

更改了代码,但再次稍微降低了性能。尝试了各种其他方法,例如使用专用奇偶校验数组来避免 parity[rp7] << 7 之后的移位;没有增益。通过使用移位运算符来更改使用奇偶校验数组的查找(例如,将 parity[rp7] << 7 替换为

rp7 ^= (rp7 << 4);
rp7 ^= (rp7 << 2);
rp7 ^= (rp7 << 1);
rp7 &= 0x80;

没有增益。

唯一的小幅更改是反转奇偶校验位,因此我们可以删除最后三个反转语句。

唉,可惜这并没有带来更多。再说一次,使用 Linux 驱动程序代码的 1000 万次迭代需要 13 到 13.5 秒,而我的代码现在需要大约 0.73 秒进行这 1000 万次迭代。因此,基本上,我在我的系统上的性能提高了 18 倍。还不错。当然,在不同的硬件上,您会得到不同的结果。不作任何保证!

但是,当然,没有免费的午餐。代码大小几乎增加了两倍(从 562 字节增加到 1434 字节)。再说一次,这并不是那么多。

纠正错误

为了纠正错误,我再次使用了 ST 应用说明作为入门,但也查看了现有代码。

该算法本身非常简单。只需异或给定的 ecc 和计算出的 ecc。如果所有字节都为 0,则没有问题。如果 11 位为 1,则我们有一个可纠正的位错误。如果只有 1 位为 1,则给定 ecc 代码中存在错误。

事实证明,进行一些查表是最快的。当必须进行修复时,此方法引入的性能提升在我的系统上约为 2 倍,如果不需要进行修复,则约为 1%。

此函数的代码大小从 330 字节增加到 686 字节。(gcc 4.2, -O3)

结论

计算 ecc 时的增益是巨大的。在我的开发硬件上,ecc 计算速度提高了 18 倍。在带有 MIPS 内核的嵌入式系统上的测试中,获得了 7 倍的提升。

在 Linksys NSLU2(ARMv5TE 处理器)的测试中,速度提升了 5 倍(大端模式,gcc 4.1.2,-O3)

对于纠正,无法获得太多增益(因为位翻转很少发生)。再说一遍,那里花费的周期也少得多。

似乎在这方面没有太多可能实现的增益,至少在用 C 编程时是这样。当然,使用汇编程序可能会从中挤出更多东西,但由于流水线行为等,这非常棘手(至少对于 intel hw)。

作者:Frans Meulenbroeks

版权所有 (C) 2008 Koninklijke Philips Electronics NV。