Skip to content

Commit 219affd

Browse files
committed
test: add a throughput benchmark
1 KiB throughput benchmark; representative numbers added to the README.
1 parent b988856 commit 219affd

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ let package = Package(
2727
.target(
2828
name: "CobsCodec"
2929
),
30+
// Dev-only throughput benchmark; not part of the shipped library.
31+
.executableTarget(
32+
name: "cobs-bench",
33+
dependencies: ["CobsCodec"]
34+
),
3035
.testTarget(
3136
name: "CobsCodecTests",
3237
dependencies: ["CobsCodec"]

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ Invalid encoded input throws `CobsDecodeError` (`.zeroByte` / `.truncated`).
9393
(basic, COBS/R, configurable-sentinel, and decode-error cases) in CI, so it
9494
interoperates exactly with the Rust, Dart, and Kotlin members of the family.
9595

96+
## Benchmarks
97+
98+
Throughput on a 1 KiB payload (`swift run -c release cobs-bench`), Swift 6.2 on an
99+
AMD Ryzen 7 3800XT under WSL2 — indicative (includes result allocation):
100+
101+
| Operation | Throughput |
102+
| --------- | ---------- |
103+
| `Cobs.encode` | ~1090 MB/s |
104+
| `Cobs.decode` | ~1150 MB/s |
105+
| `Cobsr.encode` | ~850 MB/s |
106+
96107
## Background
97108

98109
Stuart Cheshire and Mary Baker, "Consistent Overhead Byte Stuffing",

Sources/cobs-bench/main.swift

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//
2+
// main.swift
3+
// cobs-bench
4+
//
5+
// A small throughput benchmark for the CobsCodec library.
6+
//
7+
// Times `Cobs.encode`, `Cobs.decode`, and `Cobsr.encode` over a fixed 1 KiB
8+
// payload and reports throughput in MB/s (payload bytes / wall time). This is
9+
// dev-only tooling; it is not part of the shipped library.
10+
//
11+
12+
import CobsCodec
13+
import Foundation
14+
15+
/// Number of payload bytes benchmarked per operation (1 KiB).
16+
let payloadLength = 1024
17+
18+
/// Iterations timed per measurement.
19+
let iterations = 2_000_000
20+
21+
/// Iterations run untimed before each measurement to warm caches and the JIT-free
22+
/// release code paths.
23+
let warmupIterations = 100_000
24+
25+
/// Builds a deterministic 1 KiB payload that is mostly non-zero with roughly one
26+
/// in eight bytes set to `0x00`, so the COBS block-splitting path is exercised.
27+
///
28+
/// A 64-bit linear-congruential generator produces the pseudo-random stream; the
29+
/// fixed seed makes the payload identical on every run.
30+
func makePayload() -> [UInt8] {
31+
var state: UInt64 = 0x9E37_79B9_7F4A_7C15
32+
func next() -> UInt64 {
33+
state = state &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407
34+
return state
35+
}
36+
37+
var payload = [UInt8](repeating: 0, count: payloadLength)
38+
for i in 0..<payloadLength {
39+
let r = next()
40+
if (r >> 32) & 0x7 == 0 {
41+
// ~1 in 8 bytes is zero.
42+
payload[i] = 0
43+
} else {
44+
// Otherwise a guaranteed-non-zero byte in 1...255.
45+
payload[i] = UInt8((r >> 40) % 255) + 1
46+
}
47+
}
48+
return payload
49+
}
50+
51+
/// Runs `body` `iterations` times under a monotonic clock, folding each returned
52+
/// value into an accumulator so the optimizer cannot discard the work.
53+
///
54+
/// - Returns: The elapsed wall time and the accumulated sink value.
55+
func timeLoop(_ body: () -> UInt64) -> (elapsed: Duration, sink: UInt64) {
56+
var sink: UInt64 = 0
57+
let clock = ContinuousClock()
58+
let start = clock.now
59+
for _ in 0..<iterations {
60+
sink = sink &+ body()
61+
}
62+
return (clock.now - start, sink)
63+
}
64+
65+
/// Converts a `Duration` to seconds as a `Double`.
66+
func seconds(_ duration: Duration) -> Double {
67+
let components = duration.components
68+
return Double(components.seconds) + Double(components.attoseconds) / 1e18
69+
}
70+
71+
/// Throughput in MB/s, where MB is 1_000_000 bytes and the byte count is the
72+
/// payload size times the iteration count.
73+
func megabytesPerSecond(_ duration: Duration) -> Double {
74+
let totalBytes = Double(iterations) * Double(payloadLength)
75+
return totalBytes / seconds(duration) / 1e6
76+
}
77+
78+
let payload = makePayload()
79+
let encoded = Cobs.encode(payload)
80+
let zeroCount = payload.lazy.filter { $0 == 0 }.count
81+
82+
// Warm up each measured operation.
83+
for _ in 0..<warmupIterations {
84+
_ = Cobs.encode(payload)
85+
_ = try! Cobs.decode(encoded)
86+
_ = Cobsr.encode(payload)
87+
}
88+
89+
let encodeResult = timeLoop { UInt64(Cobs.encode(payload).count) }
90+
let decodeResult = timeLoop { UInt64(try! Cobs.decode(encoded).count) }
91+
let cobsrResult = timeLoop { UInt64(Cobsr.encode(payload).count) }
92+
93+
// Keep the sinks observable so the compiler retains the timed work.
94+
let sink = encodeResult.sink &+ decodeResult.sink &+ cobsrResult.sink
95+
96+
print("cobs-bench — CobsCodec throughput")
97+
print("payload: \(payloadLength) bytes, \(zeroCount) zero (~1 in 8)")
98+
print("encoded: \(encoded.count) bytes")
99+
print("iterations: \(iterations) (+\(warmupIterations) warmup)")
100+
print("")
101+
print(String(format: "COBS encode: %8.1f MB/s", megabytesPerSecond(encodeResult.elapsed)))
102+
print(String(format: "COBS decode: %8.1f MB/s", megabytesPerSecond(decodeResult.elapsed)))
103+
print(String(format: "COBS/R encode:%8.1f MB/s", megabytesPerSecond(cobsrResult.elapsed)))
104+
print("")
105+
print("sink: \(sink)")

0 commit comments

Comments
 (0)