|
| 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