ByteBuffer: provide multi read/write int methods (#1987)

Motivation:

Many network protocols (especially for example NFS) have quite a number
of integer values next to each other. In NIO, you'd normally parse/write
them with multiple read/writeInteger calls.

Unfortunately, that's a bit wasteful because we're checking the bounds
as well as the CoW state every time.

Modifications:

- Provide read/writeMultipleIntegers for up to 15 FixedWidthIntegers.
- Benchmarks

Result:

Faster code. For 10 UInt32s, this is a 5x performance win on my machine,
see benchmarks.
This commit is contained in:
Johannes Weiss 2021-11-22 14:58:52 +00:00 committed by GitHub
parent addf69cfe6
commit b2629903ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2421 additions and 2 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOCore
final class ByteBufferReadWriteMultipleIntegersBenchmark<I: FixedWidthInteger>: Benchmark {
private let iterations: Int
private let numberOfInts: Int
private var buffer: ByteBuffer = ByteBuffer()
init(iterations: Int, numberOfInts: Int) {
self.iterations = iterations
self.numberOfInts = numberOfInts
}
func setUp() throws {
self.buffer.reserveCapacity(self.numberOfInts * MemoryLayout<I>.size)
}
func tearDown() {
}
func run() throws -> Int {
var result: I = 0
for _ in 0..<self.iterations {
for i in I(0)..<I(10) {
self.buffer.writeInteger(i)
}
for _ in I(0)..<I(10) {
result = result &+ self.buffer.readInteger(as: I.self)!
}
}
precondition(result == I(self.iterations) * 45)
return self.buffer.readableBytes
}
}
final class ByteBufferMultiReadWriteTenIntegersBenchmark<I: FixedWidthInteger>: Benchmark {
private let iterations: Int
private var buffer: ByteBuffer = ByteBuffer()
init(iterations: Int) {
self.iterations = iterations
}
func setUp() throws {
self.buffer.reserveCapacity(10 * MemoryLayout<I>.size)
}
func tearDown() {
}
func run() throws -> Int {
var result: I = 0
for _ in 0..<self.iterations {
self.buffer.writeMultipleIntegers(
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
as: (I, I, I, I, I, I, I, I, I, I).self
)
let value = self.buffer.readMultipleIntegers(as: (I, I, I, I, I, I, I, I, I, I).self)!
result = result &+ value.0
result = result &+ value.1
result = result &+ value.2
result = result &+ value.3
result = result &+ value.4
result = result &+ value.5
result = result &+ value.6
result = result &+ value.7
result = result &+ value.8
result = result &+ value.9
}
precondition(result == I(self.iterations) * 45)
return self.buffer.readableBytes
}
}

View File

@ -814,3 +814,9 @@ try measureAndPrint(desc: "byte_to_message_decoder_decode_many_small",
measureAndPrint(desc: "generate_10k_random_request_keys") { measureAndPrint(desc: "generate_10k_random_request_keys") {
return (0 ..< 10_000).reduce(into: 0, { result, _ in result &+= NIOWebSocketClientUpgrader.randomRequestKey().count }) return (0 ..< 10_000).reduce(into: 0, { result, _ in result &+= NIOWebSocketClientUpgrader.randomRequestKey().count })
} }
try measureAndPrint(desc: "bytebuffer_rw_10_uint32s",
benchmark: ByteBufferReadWriteMultipleIntegersBenchmark<UInt32>(iterations: 1_000_000, numberOfInts: 10))
try measureAndPrint(desc: "bytebuffer_multi_rw_10_uint32s",
benchmark: ByteBufferMultiReadWriteTenIntegersBenchmark<UInt32>(iterations: 1_000_000))

View File

@ -109,8 +109,10 @@ extension NIOWebSocketClientUpgrader {
var buffer = ByteBuffer() var buffer = ByteBuffer()
buffer.reserveCapacity(minimumWritableBytes: 16) buffer.reserveCapacity(minimumWritableBytes: 16)
/// we may want to use `randomBytes(count:)` once the proposal is accepted: https://forums.swift.org/t/pitch-requesting-larger-amounts-of-randomness-from-systemrandomnumbergenerator/27226 /// we may want to use `randomBytes(count:)` once the proposal is accepted: https://forums.swift.org/t/pitch-requesting-larger-amounts-of-randomness-from-systemrandomnumbergenerator/27226
buffer.writeInteger(UInt64.random(in: UInt64.min...UInt64.max, using: &generator)) buffer.writeMultipleIntegers(
buffer.writeInteger(UInt64.random(in: UInt64.min...UInt64.max, using: &generator)) UInt64.random(in: UInt64.min...UInt64.max, using: &generator),
UInt64.random(in: UInt64.min...UInt64.max, using: &generator)
)
return String(base64Encoding: buffer.readableBytesView) return String(base64Encoding: buffer.readableBytesView)
} }
/// Generates a random WebSocket Request Key by generating 16 bytes randomly using the `SystemRandomNumberGenerator` and encoding them as a base64 string as defined in RFC6455 https://tools.ietf.org/html/rfc6455#section-4.1. /// Generates a random WebSocket Request Key by generating 16 bytes randomly using the `SystemRandomNumberGenerator` and encoding them as a base64 string as defined in RFC6455 https://tools.ietf.org/html/rfc6455#section-4.1.

View File

@ -221,6 +221,9 @@ extension ByteBufferTest {
("testHashableConformance", testHashableConformance), ("testHashableConformance", testHashableConformance),
("testInvalidHash", testInvalidHash), ("testInvalidHash", testInvalidHash),
("testValidHashFromSlice", testValidHashFromSlice), ("testValidHashFromSlice", testValidHashFromSlice),
("testWritingMultipleIntegers", testWritingMultipleIntegers),
("testReadAndWriteMultipleIntegers", testReadAndWriteMultipleIntegers),
("testAllByteBufferMultiByteVersions", testAllByteBufferMultiByteVersions),
] ]
} }
} }

View File

@ -3128,4 +3128,123 @@ extension ByteBufferTest {
XCTAssertEqual(bufferView.hashValue, comparisonBufferView.hashValue) XCTAssertEqual(bufferView.hashValue, comparisonBufferView.hashValue)
} }
func testWritingMultipleIntegers() {
let w1 = self.buf.writeMultipleIntegers(UInt32(1), UInt8(2), UInt16(3), UInt64(4), UInt16(5), endianness: .big)
let w2 = self.buf.writeMultipleIntegers(UInt32(1), UInt8(2), UInt16(3), UInt64(4), UInt16(5), endianness: .little)
XCTAssertEqual(17, w1)
XCTAssertEqual(17, w2)
let one1 = self.buf.readInteger(endianness: .big, as: UInt32.self)
let two1 = self.buf.readInteger(endianness: .big, as: UInt8.self)
let three1 = self.buf.readInteger(endianness: .big, as: UInt16.self)
let four1 = self.buf.readInteger(endianness: .big, as: UInt64.self)
let five1 = self.buf.readInteger(endianness: .big, as: UInt16.self)
let one2 = self.buf.readInteger(endianness: .little, as: UInt32.self)
let two2 = self.buf.readInteger(endianness: .little, as: UInt8.self)
let three2 = self.buf.readInteger(endianness: .little, as: UInt16.self)
let four2 = self.buf.readInteger(endianness: .little, as: UInt64.self)
let five2 = self.buf.readInteger(endianness: .little, as: UInt16.self)
XCTAssertEqual(1, one1)
XCTAssertEqual(1, one2)
XCTAssertEqual(2, two1)
XCTAssertEqual(2, two2)
XCTAssertEqual(3, three1)
XCTAssertEqual(3, three2)
XCTAssertEqual(4, four1)
XCTAssertEqual(4, four2)
XCTAssertEqual(5, five1)
XCTAssertEqual(5, five2)
XCTAssertEqual(self.buf.readableBytes, 0)
}
func testReadAndWriteMultipleIntegers() {
for endianness in [Endianness.little, .big] {
let v1: UInt8 = .random(in: .min ... .max)
let v2: UInt16 = .random(in: .min ... .max)
let v3: UInt32 = .random(in: .min ... .max)
let v4: UInt64 = .random(in: .min ... .max)
let v5: UInt64 = .random(in: .min ... .max)
let v6: UInt32 = .random(in: .min ... .max)
let v7: UInt16 = .random(in: .min ... .max)
let v8: UInt8 = .random(in: .min ... .max)
let v9: UInt16 = .random(in: .min ... .max)
let v10: UInt32 = .random(in: .min ... .max)
let startWriterIndex = self.buf.writerIndex
let written = self.buf.writeMultipleIntegers(
v1, v2, v3, v4, v5, v6, v7, v8, v9, v10,
endianness: endianness,
as: (UInt8, UInt16, UInt32, UInt64, UInt64, UInt32, UInt16, UInt8, UInt16, UInt32).self)
XCTAssertEqual(startWriterIndex + written, self.buf.writerIndex)
XCTAssertEqual(written, self.buf.readableBytes)
let result = self.buf.readMultipleIntegers(endianness: endianness,
as: (UInt8, UInt16, UInt32, UInt64, UInt64, UInt32, UInt16, UInt8, UInt16, UInt32).self)
XCTAssertNotNil(result)
XCTAssertEqual(0, self.buf.readableBytes)
XCTAssertEqual(v1, result?.0, "endianness: \(endianness)")
XCTAssertEqual(v2, result?.1, "endianness: \(endianness)")
XCTAssertEqual(v3, result?.2, "endianness: \(endianness)")
XCTAssertEqual(v4, result?.3, "endianness: \(endianness)")
XCTAssertEqual(v5, result?.4, "endianness: \(endianness)")
XCTAssertEqual(v6, result?.5, "endianness: \(endianness)")
XCTAssertEqual(v7, result?.6, "endianness: \(endianness)")
XCTAssertEqual(v8, result?.7, "endianness: \(endianness)")
XCTAssertEqual(v9, result?.8, "endianness: \(endianness)")
XCTAssertEqual(v10, result?.9, "endianness: \(endianness)")
}
}
func testAllByteBufferMultiByteVersions() {
let i = UInt8(86)
self.buf.writeMultipleIntegers(i, i)
self.buf.writeMultipleIntegers(i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i, i, i, i, i, i)
self.buf.writeMultipleIntegers(i, i, i, i, i, i, i, i, i, i, i, i, i, i, i)
XCTAssertEqual(Array(repeating: UInt8(86), count: 119), Array(self.buf.readableBytesView))
var values2 = self.buf.readMultipleIntegers(as: (UInt8, UInt8).self)!
var values3 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8).self)!
var values4 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8).self)!
var values5 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values6 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values7 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values8 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values9 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values10 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values11 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values12 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values13 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values14 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
var values15 = self.buf.readMultipleIntegers(as: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)!
XCTAssertEqual([i, i], withUnsafeBytes(of: &values2, { Array($0) }))
XCTAssertEqual([i, i, i], withUnsafeBytes(of: &values3, { Array($0) }))
XCTAssertEqual([i, i, i, i], withUnsafeBytes(of: &values4, { Array($0) }))
XCTAssertEqual([i, i, i, i, i], withUnsafeBytes(of: &values5, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i], withUnsafeBytes(of: &values6, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i], withUnsafeBytes(of: &values7, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values8, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values9, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values10, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values11, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values12, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values13, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values14, { Array($0) }))
XCTAssertEqual([i, i, i, i, i, i, i, i, i, i, i, i, i, i, i], withUnsafeBytes(of: &values15, { Array($0) }))
XCTAssertEqual(0, self.buf.readableBytes)
}
} }

View File

@ -0,0 +1,169 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the SwiftNIO open source project
##
## Copyright (c) 2021 Apple Inc. and the SwiftNIO project authors
## Licensed under Apache License v2.0
##
## See LICENSE.txt for license information
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
##
## SPDX-License-Identifier: Apache-2.0
##
##===----------------------------------------------------------------------===##
set -eu
here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
function gen() {
how_many=$1
# READ
echo " @inlinable"
echo " @_alwaysEmitIntoClient"
echo -n " public mutating func readMultipleIntegers<T1: FixedWidthInteger"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n: FixedWidthInteger"
done
echo -n ">("
echo -n "endianness: Endianness = .big, as: (T1"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n"
done
echo -n ").Type = (T1"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n"
done
echo -n ").self) -> (T1"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n"
done
echo ")? {"
echo " var bytesRequired: Int = MemoryLayout<T1>.size"
for n in $(seq 2 "$how_many"); do
echo " bytesRequired &+= MemoryLayout<T$n>.size"
done
echo
echo " guard self.readableBytes >= bytesRequired else {"
echo " return nil"
echo " }"
echo
for n in $(seq 1 "$how_many"); do
echo " var v$n: T$n = 0"
done
echo " var offset = 0"
echo " self.readWithUnsafeReadableBytes { ptr -> Int in"
echo " assert(ptr.count >= bytesRequired)"
echo " let basePtr = ptr.baseAddress! // safe, ptr is non-empty"
for n in $(seq 1 "$how_many"); do
echo " withUnsafeMutableBytes(of: &v$n) { destPtr in"
echo " destPtr.baseAddress!.copyMemory(from: basePtr + offset, byteCount: MemoryLayout<T$n>.size)"
echo " }"
echo " offset = offset &+ MemoryLayout<T$n>.size"
done
echo " assert(offset == bytesRequired)"
echo " return offset"
echo " }"
echo " switch endianness {"
for endianness in big little; do
echo " case .$endianness:"
echo -n " return (T1(${endianness}Endian: v1)"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n(${endianness}Endian: v$n)"
done
echo ")"
done
echo " }"
echo " }"
echo
# WRITE
echo " @inlinable"
echo " @_alwaysEmitIntoClient"
echo " @discardableResult"
echo -n " public mutating func writeMultipleIntegers<T1: FixedWidthInteger"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n: FixedWidthInteger"
done
echo -n ">(_ value1: T1"
for n in $(seq 2 "$how_many"); do
echo -n ", _ value$n: T$n"
done
echo -n ", endianness: Endianness = .big, as: (T1"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n"
done
echo -n ").Type = (T1"
for n in $(seq 2 "$how_many"); do
echo -n ", T$n"
done
echo ").self) -> Int {"
for n in $(seq 1 "$how_many"); do
echo " var v$n: T$n"
done
echo " switch endianness {"
for endianness in .big .little; do
echo " case $endianness:"
for n in $(seq 1 "$how_many"); do
echo " v$n = value$n${endianness}Endian"
done
done
echo " }"
echo
echo " var spaceNeeded: Int = MemoryLayout<T1>.size"
for n in $(seq 2 "$how_many"); do
echo " spaceNeeded &+= MemoryLayout<T$n>.size"
done
echo
echo " return self.writeWithUnsafeMutableBytes(minimumWritableBytes: spaceNeeded) { ptr -> Int in"
echo " assert(ptr.count >= spaceNeeded)"
echo " var offset = 0"
echo " let basePtr = ptr.baseAddress! // safe: pointer is non zero length"
for n in $(seq 1 "$how_many"); do
echo " (basePtr + offset).copyMemory(from: &v$n, byteCount: MemoryLayout<T$n>.size)"
echo " offset = offset &+ MemoryLayout<T$n>.size"
done
echo " assert(offset == spaceNeeded)"
echo " return offset"
echo " }"
echo " }"
echo
}
grep -q "ByteBuffer" "${BASH_SOURCE[0]}" || {
echo >&2 "ERROR: ${BASH_SOURCE[0]}: file or directory not found (this should be this script)"
exit 1
}
{
cat <<"EOF"
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
/// NOTE: THIS FILE IS AUTO-GENERATED BY dev/generate-bytebuffer-multi-int.sh
EOF
echo
echo "extension ByteBuffer {"
# note:
# - widening the inverval below (eg. going from {2..15} to {2..25}) is Semver minor
# - narrowing the interval below is SemVer _MAJOR_!
for n in {2..15}; do
gen "$n"
done
echo "}"
} > "$here/../Sources/NIOCore/ByteBuffer-multi-int.swift"