Files
Jamie LiuandgVisor bot 40a662086b netstack: further optimize checksumming
- Restructure calculateChecksumNoASM() to consistently handle misaligned
  inputs: special-case inputs less than 8 bytes in size, otherwise align the
  buffer address to 8 bytes before checksumming in bulk. This avoids
  significant performance degradation on misaligned inputs due to fallback to
  unrolledCalculateChecksum().

- Sum up to 64 bytes per loop iteration in calculateChecksumNoASM(), as in
  unrolledCalculateChecksum(). This significantly improves performance for
  large inputs.

- Don't plumb carry between blocks in calculateChecksumNoASM(). On x86, this is
  significantly faster because many instructions clobber the carry flag, so
  saving it requires extra overhead; on arm64, this has minimal effect on the
  generated code (the compiler inserts an extra useless copy from the zero
  register to a temporary).

- Replace the folding of uint64 accumulator to uint16 checksum with a
  branchless equivalent, which is ~2ns/checksum faster on average due to
  avoiding an unpredictable loop.

- Fix calculateChecksumNoASM() on big-endian architectures.

- Delete unrolledCalculateChecksum(), which is now unused.

Before these changes, calculateChecksumAMD64() is approximately twice as fast
as calculateChecksumNoASM(). After these changes, calculateChecksumNoASM() is
faster than calculateChecksumAMD64():

```
goos: linux
goarch: amd64
pkg: pkg/tcpip/checksum/checksum
cpu: Intel(R) Xeon(R) CPU @ 2.80GHz
                           │ checksum_baseline │         checksum_experiment         │
                           │      sec/op       │   sec/op     vs base                │
Checksum/checksum_64-48            10.50n ± 0%   10.10n ± 0%   -3.86% (p=0.000 n=10)
Checksum/checksum_128-48           13.72n ± 0%   12.16n ± 0%  -11.37% (p=0.000 n=10)
Checksum/checksum_256-48           22.80n ± 0%   16.31n ± 0%  -28.48% (p=0.000 n=10)
Checksum/checksum_512-48           39.92n ± 0%   24.96n ± 0%  -37.47% (p=0.000 n=10)
Checksum/checksum_1024-48          81.34n ± 0%   44.26n ± 2%  -45.58% (p=0.000 n=10)
Checksum/checksum_1500-48         119.70n ± 1%   65.05n ± 2%  -45.65% (p=0.000 n=10)
Checksum/checksum_2048-48         157.30n ± 0%   86.65n ± 0%  -44.91% (p=0.000 n=10)
Checksum/checksum_4096-48          309.5n ± 0%   172.4n ± 3%  -44.30% (p=0.000 n=10)
Checksum/checksum_8192-48          615.0n ± 0%   348.0n ± 0%  -43.42% (p=0.000 n=10)
Checksum/checksum_16384-48        1222.5n ± 0%   690.8n ± 0%  -43.49% (p=0.000 n=10)
Checksum/checksum_32767-48         2.447µ ± 0%   1.381µ ± 0%  -43.55% (p=0.000 n=10)
Checksum/checksum_32768-48         2.442µ ± 0%   1.381µ ± 0%  -43.48% (p=0.000 n=10)
Checksum/checksum_65535-48         4.883µ ± 0%   2.933µ ± 0%  -39.94% (p=0.000 n=10)
Checksum/checksum_65536-48         4.877µ ± 0%   2.935µ ± 0%  -39.82% (p=0.000 n=10)
geomean                            260.6n        161.9n       -37.88%
```

Thus, use calculateChecksumNoASM() on all architectures and rename it to
calculateChecksum().

---

Note: As of this writing, calculateChecksumNoASM() is still ~7% slower than an
equivalently-optimized x86 assembly implementation, for three main reasons:

1. The Go compiler emits separate MOV memory-to-register and ADD/ADC
   instructions rather than using memory operands for the latter.

2. For statements of the form `buf = buf[1:]`, updating the slice data pointer
   takes ~5 instructions instead of 1, because the compiler avoids incrementing
   the pointer if the resulting slice length is 0 (such that the pointer would
   point outside of the underlying array). This overhead can be avoided by
   using uintptr and runtime.KeepAlive(), but doing so worsens problem (1):
   replacing `*(*uint64(unsafe.Pointer(&buf[8])))` with
   `*(*uint64(unsafe.Pointer(addr+8)))` causes the compiler to split each MOV
   into LEA + MOV.

3. The faster form of reduce() described in that function is implementable in
   assembly.

Thus, we can add the optimized x86 assembly implementation back if desired.

---

`GOOS=linux GOARCH=mips64 go test -exec qemu-mips64 ./...` fails before this
CL, since the "bitslib" implementation, i.e. calculateChecksumNoASM(), is
broken on big-endian architectures. After this CL:
```
ok      gvisor.dev/gvisor/pkg/tcpip/checksum    125.970s
```

PiperOrigin-RevId: 574264329
2023-10-17 14:25:28 -07:00

69 lines
1.9 KiB
Go

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package checksum provides the implementation of the encoding and decoding of
// network protocol headers.
package checksum
import (
"encoding/binary"
)
// Size is the size of a checksum.
//
// The checksum is held in a uint16 which is 2 bytes.
const Size = 2
// Put puts the checksum in the provided byte slice.
func Put(b []byte, xsum uint16) {
binary.BigEndian.PutUint16(b, xsum)
}
// Checksum calculates the checksum (as defined in RFC 1071) of the bytes in the
// given byte array. This function uses an optimized version of the checksum
// algorithm.
//
// The initial checksum must have been computed on an even number of bytes.
func Checksum(buf []byte, initial uint16) uint16 {
s, _ := calculateChecksum(buf, false, initial)
return s
}
// Checksumer calculates checksum defined in RFC 1071.
type Checksumer struct {
sum uint16
odd bool
}
// Add adds b to checksum.
func (c *Checksumer) Add(b []byte) {
if len(b) > 0 {
c.sum, c.odd = calculateChecksum(b, c.odd, c.sum)
}
}
// Checksum returns the latest checksum value.
func (c *Checksumer) Checksum() uint16 {
return c.sum
}
// Combine combines the two uint16 to form their checksum. This is done
// by adding them and the carry.
//
// Note that checksum a must have been computed on an even number of bytes.
func Combine(a, b uint16) uint16 {
v := uint32(a) + uint32(b)
return uint16(v + v>>16)
}