TCPIP校验值的优化

原文: http://locklessinc.com/articles/tcp_checksum/

TCP/IP 校验优化

    TCP/IP校验值用来检测经过TCP/IPV4传输的数据的正确性. 如果一个位发生了翻转, 或其他原因导致数据被破坏, 则数据接收者可以因为校验值不一致而发现数据被破坏(缺失). 这提供了端对端的数据正确性保证.

    IPV4使用校验值来检测包头数据的正确性. 即 源, 目的和其他元数据. TCP协议包含了一项额外的校验值用以保证载荷数据的正确性. 另IPV6没有校验值, 而是假定更底层的协议或更上层的协议会包含检验.

    TCP与IPV4所使用的检验算法是完全一致的.计算过程中一次处理一个字的数据.(如果数据的长度是奇数位的, 则在其末尾添加一个字节的0使其长度为偶数). 首先将保存结果的位置置0, 然后按字使用1的补码相加, 并将其结果取反后存储在即定位置. 重复以上过程, 结果则为0.

    上面描述的算法有许多重要的特征. 首先计算是基于加法的, 因此满足结合性与交换性. 也就是说我们也许可以通过改变计算的顺序来提高效率.

    第二个特征则更为微秒: 结果是端序无关的. 1的补码加法可以在2的补码加法机器上通过2的补码加法计算,只要将可能的进位再加到结果上即可. 也就是有点循环进位的意思.这个特性使得无论我们使用何种端序, 其计算结果将是一致的.

    最后一个特性是计算是基于加法的, 因此可以以非常低的代价更新检验值. 我们只需要对改变的位计算1的补码加法. 比如, 一个更新TTL的过程, 不必要对整个数据进行重新计算.

    对校验值的计算可以成为网络应用的瓶颈. 因此有这么多的网卡提供硬件级的计算. 但也有许多的环境使用CPU来计算, 此时优化将是非常重要的.而现在将讲讲如何优化.

TCP/IP校验算法

    C语言里2的补码使用无符号数, 并且(C语言)也不能指定有符号数的格式. 因此我们不能直接计算1的补码. 同时也没法读取进位或是使用ADC指令. 但如果我们使用大于一个字来存放和, 则可以累积多个进位. 因为加法具有交换与结合性, 我们可以在最后将进位合并到低16位中. 具体的实现应该如下:

unsignedshort checksum1(constchar*buf,unsigned size)

{

    unsigned sum =0;

    int i;

 

    /* Accumulate checksum */

    for(i =0; i < size -1; i +=2)

    {

       unsignedshort word16 =*(unsignedshort*)&buf[i];

       sum += word16;

    }

 

    /* Handle odd-sized case */

    if(size &1)

    {

       unsignedshort word16 =(unsignedchar) buf[i];

       sum += word16;

    }

 

    /* Fold to get the ones-complement result*/

    while(sum >>16) sum =(sum &0xFFFF)+(sum >>16);

 

    /* Invert to get the negative inones-complement arithmetic */

    return~sum;

}

我们将上面的片段运行2^24次并统计消耗时间以衡量其性能. 显然结果会因为实验数据的大小而变化. 我们分别选择64字节, 1023字节, 1024字节长度的数据计算三次. 64字节的结果表明小数据量时的消耗, 而1024字节的结果则会显示出在大数据量时的性能, 1023字节可以实验也奇数大小长度对性能的影响. 实验结果如下:

Size

64

1023

1024

Time (s)

0.88

10.99

11.03

    上面的算法一次处理2个字节. 一般地, 如果所使用的处理器一次处理的数据更多, 则性能会更好. 因为我们所使用的计算机是64位的, 我们更偏向于改进算法使得一次处理更多的数据. 问题是该如何处理进位. 幸运的是, 编写一个能检测进位的算法不是特别困难. 如果a + b < a, 则说明发生了进位. 当发生进位时, 则向结果多加1. 最后将64位的结果向下叠加到16位. 最后算法可能是:

unsignedshort checksum2(constchar*buf,unsigned size)

{

    unsignedlonglong sum =0;

    constunsignedlonglong*b =(unsignedlonglong*) buf;

 

    unsigned t1, t2;

    unsignedshort t3, t4;

 

    /* Main loop - 8 bytes at a time */

    while(size >=sizeof(unsignedlonglong))

    {

        unsignedlonglong s =*b++;

        sum += s;

        if(sum < s) sum++;

        size -=8;

    }

 

    /* Handle tail less than 8-bytes long */

    buf =(constchar*) b;

    if(size &4)

    {

        unsigned s =*(unsigned*)buf;

        sum += s;

        if(sum < s) sum++;

        buf +=4;

    }

 

    if(size &2)

    {

        unsignedshort s =*(unsignedshort*) buf;

        sum += s;

        if(sum < s) sum++;

        buf +=2;

    }

 

    if(size & 1)[a1] 

    {

        unsignedchar s =*(unsignedchar*) buf;

        sum += s;

        if(sum < s) sum++;

    }

 

    /* Fold down to 16 bits */

    t1 = sum;

    t2 = sum >>32;

    t1 += t2;

    if(t1 < t2) t1++;

    t3 = t1;

    t4 = t1 >>16;

    t3 += t4;

    if(t3 < t4) t3++;

 

    return~t3;

}

与期望的一样, 这一次的结果稍微快一点:

Size

64

1023

1024

Time (s)

0.29

2.90

2.93

性能并没有与处理量呈线性关系. 但接近于线性. 现在我们想要更快. 但是C没有存取进位的能力, 想要描述清楚我们想让计算机做什么变得极其困难. 因此, 我们选择直接使用汇编语言, 其可以让我们使用所有技术.

    将上面的C语言直接翻译成汇编语言其结果如下:

.globlchecksum3

.type checksum3,@function

.align16

checksum3:

    xor %eax, %eax

    cmp $8, %esi

    jl2f

 

#The main loop

1:  add(%rdi), %rax

    adc $0, %rax

    add $8, %rdi

    sub $8, %esi

    cmp $8, %esi

    jge1b

 

#Handle the tail

2:  test $4, %esi

    je  3f

    movl (%rdi), %edx

    add %rdx, %rax

    adc $0, %rax

    add $4, %rdi

 

3:  test $2, %esi

    je4f

    xor %edx, %edx

    movw (%rdi), %dx

    add %rdx, %rax

    adc $0, %rax

    add $2, %rdi

 

4:  test $1, %esi

    je5f

    xor %edx, %edx

    movb (%rdi), %dl

    add %rdx, %rax

    adc $0, %rax

 

#Fold down to 16-bits

5:  mov %eax, %edx

    shr $32, %rax

    add %edx, %eax

    adc $0, %eax

    mov %eax, %edx

    shr $16, %eax

    add %dx, %ax

    adc $0, %ax

 

#Invert to get the final checksum

    not %ax

    retq

.sizechecksum3, .-checksum3

    主循环相当简单. 其使用add指令累积结果到rax. 进位使用其后的adc指令处理. 其余的代码仅仅是更新数据指针, 递减迭代计数器. 处理最后没有成倍时的数据. 使用adc指定也很容易叠加最后的结果到16位.

    因为上面的代码是对原来C代码的直接翻译, 我们不指望有多少性能提升. 测试结果如下:

Size

64

1023

1024

Time (s)

0.28

2.88

2.91

    因为使用汇编语言的固有开销少于C语言, 可以看到还是有少许提升的. 要想计算更快, 我们需要使用更加灵活的算法. 最容易想到的是能不能对主循环优化更多.  不难发现, 我们不必每次都处理进位. 我们可以去掉主循环中的adc指令, 而是在主循环结束后使用一条adc指令处理进位. 现在的主要问题是更新数据指针时会影响(清除)进位, 因此我们必须使用新指令来更新进位(lea), 使用dec指令来递减循环计数. 这两条指令都不会改变进位.

.globlchecksum4

.type checksum4,@function

.align16

checksum4:

    mov %esi, %ecx

    xor %eax, %eax

 

#Divide by 8 to get the total number of iterations

    shr $3, %ecx

    je2f

   

#Clear the carry before starting the loop

    clc

   

#The new smaller main loop

1:  adc(%rdi), %rax

    lea8(%rdi), %rdi

    dec %ecx

    jne1b

 

#Fold in the final carry

    adc $0, %rax

   

2:  test $4, %esi

    je  3f

    movl (%rdi), %edx

    add %rdx, %rax

    adc $0, %rax

    add $4, %rdi

 

3:  test $2, %esi

    je4f

    xor %edx, %edx

    movw (%rdi), %dx

    add %rdx, %rax

    adc $0, %rax

    add $2, %rdi

 

4:  test $1, %esi

    je5f

    xor %edx, %edx

    movb (%rdi), %dl

    add %rdx, %rax

    adc $0, %rax

 

5:  mov %eax, %edx

    shr $32, %rax

    add %edx, %eax

    adc $0, %eax

    mov %eax, %edx

    shr $16, %eax

    add %dx, %ax

    adc $0, %ax

 

    not %ax

    retq

.sizechecksum4, .-checksum4

    上面的代码对主循环进行了大大的简化, 显然应当跑得快很多. 但现实往往是残酷的:

Size

64

1023

1024

Time (s)

0.27

2.87

2.90

    妈的, 修改似乎一点影响都没有. 这也表明adc是非常快的指令. 主循环中的adc 指令与其他指令有某种重叠, 因此移出adc 指令并没有太多的性能提升. 如果真如此, 那么循环展开应该会有效:

.globlchecksum5

.type checksum5,@function

.align16

checksum5:

   

    mov %esi, %ecx

    xor %eax, %eax

#Now handle 16 bytes at a time

    shr $4, %ecx

    je2f

    clc

 

#The main loop now uses two 64-bit additions

1:  adc(%rdi), %rax

    adc8(%rdi), %rax

    lea16(%rdi), %rdi

    dec %ecx

    jne1b

   

    adc $0, %rax

 

# Weneed to handle anything up to 15 tail bytes.

2:  test $8, %esi

    je  3f

    add(%rdi), %rax

    adc $0, %rax

    add $8, %rdi

   

3:  test $4, %esi

    je  4f

    movl (%rdi), %edx

    add %rdx, %rax

    adc $0, %rax

    add $4, %rdi

 

4:  test $2, %esi

    je5f

    xor %edx, %edx

    movw (%rdi), %dx

    add %rdx, %rax

    adc $0, %rax

    add $2, %rdi

 

5:  test $1, %esi

    je6f

    xor %edx, %edx

    movb (%rdi), %dl

    add %rdx, %rax

    adc $0, %rax

 

#   Since we accumulate with 64-bits still,this doesn't change.

6:  mov %eax, %edx

    shr $32, %rax

    add %edx, %eax

    adc $0, %eax

    mov %eax, %edx

    shr $16, %eax

    add %dx, %ax

    adc $0, %ax

 

    not %ax

    retq

.sizechecksum5, .-checksum5

    这一次, 我们的怀疑得到了验证, 性能得到了巨大的提升:

Size

64

1023

1024

Time (s)

0.20

1.54

1.56

    自然想到, 如果展开2倍有提升, 展开更多是否会更快. 如果我们展开4倍, 得到:

.globlchecksum6

.type checksum6,@function

.align16

checksum6:

    mov %esi, %ecx

    xor %eax, %eax

 

# 32 bytes at a time

    shr $5, %ecx

    je2f

    clc

 

#Lets make sure our quite-largeloop is now aligned

.align16

 

#Four 64-bit adds per iteration

1:  adc(%rdi), %rax

    adc8(%rdi), %rax

    adc16(%rdi), %rax

    adc24(%rdi), %rax

    lea32(%rdi), %rdi

    dec %ecx

    jne1b

   

    adc $0, %rax

 

#Handle the 31 bytes or less remaining

2:  test $16, %esi

    je  3f

    add(%rdi), %rax

    adc8(%rdi), %rax

    adc $0, %rax

    add $16, %rdi  

   

3:  test $8, %esi

    je  4f

    add(%rdi), %rax

    adc $0, %rax

    add $8, %rdi

   

4:  test $4, %esi

    je  5f

    movl (%rdi), %edx

    add %rdx, %rax

    adc $0, %rax

    add $4, %rdi

 

5:  test $2, %esi

    je6f

    xor %edx, %edx

    movw (%rdi), %dx

    add %rdx, %rax

    adc $0, %rax

    add $2, %rdi

 

6:  test $1, %esi

    je7f

    xor %edx, %edx

    movb (%rdi), %dl

    add %rdx, %rax

    adc $0, %rax

 

7:  mov %eax, %edx

    shr $32, %rax

    add %edx, %eax

    adc $0, %eax

    mov %eax, %edx

    shr $16, %eax

    add %dx, %ax

    adc $0, %ax

 

    not %ax

    retq

.sizechecksum6, .-checksum6

    是的, 性能的确得到了提升, 但是并没有上一次多. 但已值得:

Size

64

1023

1024

Time (s)

0.18

1.10

1.08

    继续展开似乎不再有帮助. 为了进一步提升速度, 我们需要使用其他的优化方法. 循环展开是增加了相关指令段(顺序)的长度. 如果我们可以处理次数进一步缩减, CPU也许可能得到更高的并行度. 不幸的是, Intel只提供了一个进位. 但我们仍可以结合使用原始C代码中的一些技术. 使用lea指令将结果累加到更宽的寄存器, 我们可以进行更多的并行计算而之间没有相互影响. 代码如下:

.globlchecksum7

.type checksum7,@function

.align16

checksum7:

    mov %esi, %ecx

    xor %eax, %eax

    shr $5, %ecx

    je2f

 

#Use %r8 to accumulate as well

    xor %r8, %r8

 

.align16

1:  movl 24(%rdi), %edx

# Add32-bitsinto %r8

    lea(%rdx,%r8),%r8

#Three adc's now into %rax instead of four

    adc(%rdi), %rax

    adc8(%rdi), %rax

    adc16(%rdi), %rax

    movl 28(%rdi), %edx

# Add another 32-bitsinto %r8

    lea(%rdx,%r8),%r8

    lea32(%rdi), %rdi

    dec %ecx

    jne1b

 

#Merge %r8 with other results in %rax

    adc %r8, %rax

    adc $0, %rax

2:  test $16, %esi

    je  3f

    add(%rdi), %rax

    adc8(%rdi), %rax

    adc $0, %rax

    add $16, %rdi  

   

3:  test $8, %esi

    je  4f

    add(%rdi), %rax

    adc $0, %rax

    add $8, %rdi

   

4:  test $4, %esi

    je  5f

    movl (%rdi), %edx

    add %rdx, %rax

    adc $0, %rax

    add $4, %rdi

 

5:  test $2, %esi

    je6f

    xor %edx, %edx

    movw (%rdi), %dx

    add %rdx, %rax

    adc $0, %rax

    add $2, %rdi

 

6:  test $1, %esi

    je7f

    xor %edx, %edx

    movb (%rdi), %dl

    add %rdx, %rax

    adc $0, %rax

 

7:  mov %eax, %edx

    shr $32, %rax

    add %edx, %eax

    adc $0, %eax

    mov %eax, %edx

    shr $16, %eax

    add %dx, %ax

    adc $0, %ax

 

    not %ax

    retq

.sizechecksum7, .-checksum7

那么, 有效果吗? 好像并没有:

Size

64

1023

1024

Time (s)

0.22

1.10

1.15

甚至比之前的结果要更差些. 有时, 所谓的巧秒会有帮助, 有时不会. 我们将会使用一些其他技术来提升性能.

不懂SIMD, 下面的部分以后再翻译.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值