Skip to content

Commit 3f23447

Browse files
hassilaMahdiBMalexandersandbergcthielen
committed
Benchmark blog post (swiftlang#460)
* Adding placeholder for blog post + updating author file * First iteration * Link to latest documentation * Add link to Swift Certificates benchmark workflow * Add link to Instruments integration * Add link to community section at Swift forums * Clarify language * Fix SPI links to be version-independent * Addrees PR feedback * Update _posts/2023-12-15-benchmarks.md Co-authored-by: Mahdi Bahrami <[email protected]> * Address PR feedback, fix image background * Fix wording * Update _posts/2023-12-15-benchmarks.md Co-authored-by: Alexander Sandberg <[email protected]> * Apply suggestions from code review Co-authored-by: Alexander Sandberg <[email protected]> * Use comma consistently after e.g. * Compress png image * Feature -> Requirement * Put placeholder date into the future again * Add newline in the intro for nicer display * Put placeholder date into the future again * Address PR feedback * Address PR feedback * Add some background information on package origin as request * Add Swift Package Manager as another package that have adopted Benchmark * Fix authors for rebase * Try to address PR feedback from cthielen * Update publish date/time --------- Co-authored-by: Mahdi Bahrami <[email protected]> Co-authored-by: Alexander Sandberg <[email protected]> Co-authored-by: Christopher Thielen <[email protected]>
1 parent 411c177 commit 3f23447

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed

_data/authors.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,6 @@ ahannun:
414414
415415
github: awni
416416

417-
418417
rcollobert:
419418
name: Ronan Collobert
420419
@@ -431,3 +430,10 @@ rauhul:
431430
432431
github: rauhul
433432
about: "Rauhul Varma works on Advanced Prototyping in the Platform Architecture group at Apple."
433+
434+
hassila:
435+
name: Joakim Hassila
436+
437+
gravatar: d6ff2f8e32e6155569a312295bce0ec8
438+
github: hassila
439+
about: "Joakim Hassila is the CTO of Ordo One, which builds high performance distributed trading systems written in Swift running on Linux and macOS."

_posts/2024-03-20-benchmarks.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
---
2+
layout: post
3+
published: true
4+
date: 2024-03-20 10:00:00
5+
title: "Introducing Swift's Benchmark Package: Complementing Unit Tests with Performance Checks"
6+
author: [hassila]
7+
---
8+
9+
In the world of software development, the old adage "make it work, make it right, make it fast" serves as a guiding principle for creating robust, efficient applications. This journey starts with ensuring that our code functions as intended, a task where unit and integration testing have proven indispensable. However, ensuring functionality is only part of the equation. The true measure of an application's excellence extends into its performance - how fast and efficiently it operates under various conditions. Herein lies the critical but often overlooked third step: _making it fast_.
10+
11+
In the realm of professional trading software, the role of a comprehensive benchmarking framework integrated with Continuous Integration (CI) parallels the importance of unit and integration testing. Just as unit and integration tests are essential for ensuring the functional correctness of software, benchmarking within a CI pipeline is crucial for continuously validating the non-functional aspects, such as high throughput, low latency, predictable performance and consistent resource usage. This is vital for maintaining the competitive edge in a fast-paced financial environment where the extreme market data rates and performance requirements means that even small variations in response time - on the scale of microseconds - can significantly impact trade outcomes.
12+
13+
Performance is an important part of the overall product regardless of the application domain, no end user wants to wait on a computer or other electronic device, instant response to user operations truly helps provide a delightful end user experience.
14+
15+
After examining the existing infrastructure within the Swift ecosystem, we concluded that there were no existing solutions meeting our needs for multi-platform and rich metrics support, CI integration, and developer-friendliness. Therefore, we decided to develop a [Benchmark package](https://github.com/ordo-one/package-benchmark) and open source it, believing it could help advance performance for the Swift community and benefit all of us.
16+
17+
### The Role Of Benchmarks
18+
19+
Have you ever encountered a _performance problem_ that slipped through to end users which resulted in a bug report? Do you systematically measure and validate performance metrics when making changes to your Swift package?
20+
21+
Swift aims for performance that rivals C-based languages, emphasizing predictable and consistent execution. Achieving this involves optimizing the use of constrained resources like CPU, memory, and network bandwidth, which significantly influence application workloads across server-side, desktop, and mobile environments. Key performance metrics include CPU usage, memory allocation and management, network I/O, and system calls, among others. These metrics are essential for foundational software, where controlling resource usage and minimizing footprint are as critical as maintaining runtime performance. The Benchmark package readily supports these metrics, along with OS-specific ones for Linux and macOS, providing a comprehensive toolkit for Swift developers to monitor and enhance their applications' efficiency.
22+
23+
Constructing a set of benchmarks and consistently running them provides an indication when something is not performing as expected, just as a unit test flags if some functional expectation is broken. Then complementary tools (e.g. Instruments, DTrace, Heaptrack, Leaks, Sample, ...) are used to for root-cause analysis to analyze and fix the underlying problem.
24+
25+
This is analogous to unit tests, where a failed test indicates that something is wrong, and other more specialized tools are used to fix the problem (e.g., a debugger, TSAN/ASAN, adding asserts, debug printouts, …).
26+
27+
### Benchmarking Infrastructure
28+
29+
The open-source [Benchmark package](https://github.com/ordo-one/package-benchmark) helps you automate performance testing and makes it easy for individual developers to run a quick performance validation locally before pushing changes.
30+
31+
The Benchmark package is implemented as a SwiftPM command plugin and adds a dedicated command to interact with benchmarks:
32+
33+
> ```swift package benchmark```
34+
35+
Introductory getting started information is available both on the [package GitHub page](https://github.com/ordo-one/package-benchmark) as well as in the [Swift Package Index DocC documentation](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted).
36+
37+
A minimalistic benchmark measuring the performance of `Date` would simply be:
38+
39+
```swift
40+
import Benchmark
41+
import Foundation
42+
43+
let benchmarks = {
44+
Benchmark("Foundation-Date") { benchmark in
45+
for _ in benchmark.scaledIterations {
46+
blackHole(Foundation.Date())
47+
}
48+
}
49+
}
50+
```
51+
52+
It is suitable both for microbenchmarks mostly concerned with CPU usage as well as for more complex long-running benchmarks and supports measuring a wide range of samples over a long time thanks to using the [HDR Histogram](https://github.com/HdrHistogram/hdrhistogram-swift) package.
53+
54+
Benchmark provides support for an [extensive set of built-in metrics](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/metrics):
55+
56+
- `cpuUser` - CPU user space time spent for running the test
57+
- `cpuSystem` - CPU system time spent for running the test
58+
- `cpuTotal` - CPU total time spent for running the test (system + user)
59+
- `wallClock` - Wall clock time for running the test
60+
- `throughput` - The throughput in operations / second
61+
- `peakMemoryResident` - The resident memory usage - sampled during runtime
62+
- `peakMemoryResidentDelta` - The resident memory usage - sampled during runtime (excluding start of benchmark baseline)
63+
- `peakMemoryVirtual` - The virtual memory usage - sampled during runtime
64+
- `mallocCountSmall` - The number of small malloc calls according to jemalloc
65+
- `mallocCountLarge` - The number of large malloc calls according to jemalloc
66+
- `mallocCountTotal` - The total number of mallocs according to jemalloc
67+
- `allocatedResidentMemory` - The amount of allocated resident memory by the application (not including allocator metadata overhead etc) according to jemalloc
68+
- `memoryLeaked` - The number of small+large mallocs - small+large frees in resident memory (just a possible leak)
69+
- `syscalls` - The number of syscalls made during the test – macOS only
70+
- `contextSwitches` - The number of context switches made during the test – _macOS only_
71+
- `threads` - The maximum number of threads in the process under the test (not exact, sampled)
72+
- `threadsRunning` - The number of threads actually running under the test (not exact, sampled) – macOS only
73+
- `readSyscalls` - The number of I/O read syscalls performed e.g. read(2) / pread(2) – _Linux only_
74+
- `writeSyscalls` - The number of I/O write syscalls performed e.g. write(2) / pwrite(2) – _Linux only_
75+
- `readBytesLogical` - The number of bytes read from storage (may be from pagecache!) – _Linux only_
76+
- `writeBytesLogical` - The number bytes written to storage (may be cached) – _Linux only_
77+
- `readBytesPhysical` - The number of bytes physically read from a block device – _Linux only_
78+
- `writeBytesPhysical` - The number of bytes physically written to a block device – _Linux only_
79+
- `retainCount` - The number of retain calls (ARC)
80+
- `releaseCount` - The number of release calls (ARC)
81+
- `retainReleaseDelta` - `abs(retainCount - releaseCount)` - if this is non-zero, it would typically mean the benchmark has a retain cycle (use Memory Graph Debugger to troubleshoot)
82+
83+
Custom metrics are supported as well for application-specific measurements (e.g. cache hit/miss statistics).
84+
85+
### Writing Benchmarks
86+
87+
There's an [introduction to writing benchmarks](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/writingbenchmarks) as well as a [sample repository](https://github.com/ordo-one/package-benchmark-samples).
88+
89+
A slightly more complicated benchmark measuring a part of the `Histogram` package:
90+
```swift
91+
import Benchmark
92+
import Foundation
93+
import Histogram
94+
95+
let benchmarks = {
96+
// Minimal benchmark with default settings
97+
Benchmark("Foundation-Date") { benchmark in
98+
for _ in benchmark.scaledIterations {
99+
blackHole(Foundation.Date())
100+
}
101+
}
102+
103+
// Slightly more complex with some customization
104+
let customBenchmarkConfiguration: Benchmark.Configuration = .init(
105+
metrics: [
106+
.wallClock,
107+
.throughput,
108+
.syscalls,
109+
.threads,
110+
.peakMemoryResident
111+
],
112+
scalingFactor: .kilo
113+
)
114+
115+
Benchmark("ValueAtPercentile", configuration: customBenchmarkConfiguration) { benchmark in
116+
let maxValue: UInt64 = 1_000_000
117+
118+
var histogram = Histogram<UInt64>(highestTrackableValue: maxValue,
119+
numberOfSignificantValueDigits: .three)
120+
121+
for _ in 0 ..< 10_000 {
122+
blackHole(histogram.record(UInt64.random(in: 10 ... 1_000)))
123+
}
124+
125+
let percentiles = [0.0, 25.0, 50.0, 75.0, 80.0, 90.0, 99.0, 100.0]
126+
127+
benchmark.startMeasurement() // don't measure the setup cost above
128+
129+
for i in benchmark.scaledIterations {
130+
blackHole(histogram.valueAtPercentile(percentiles[i % percentiles.count]))
131+
}
132+
133+
benchmark.stopMeasurement()
134+
}
135+
}
136+
```
137+
138+
### Benchmark Output And Analytics
139+
140+
The default output is in a table format for human readability, but the package [supports a range of different output formats](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/exportingbenchmarks) with output suitable for analysis with other visualization tools.
141+
142+
Sample default output when running benchmarks:
143+
<img
144+
alt="Sample text output for benchmarks"
145+
src="/assets/images/benchmark-blog/Benchmark.png" />
146+
147+
### Key Benchmark Workflows Are Supported
148+
149+
* *[Automated Pull Request performance regression checks](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/comparingbenchmarksci)* by comparing the performance metrics of a pull request with the main branch and having the PR workflow check fail if there is a regression according to absolute or relative thresholds specified per benchmark
150+
* Automated Pull Request check vs. a pre-recorded *absolute baseline p90 threshold* (see e.g., [Swift Certificates](https://github.com/apple/swift-certificates/tree/main/Benchmarks) for such a workflow with [related Docker files](https://github.com/apple/swift-certificates/tree/main/docker)), suitable for e.g., malloc regression tests
151+
* *[Manual comparison of multiple performance baselines](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/creatingandcomparingbaselines)* for iterative or A/B performance work by an individual developer
152+
* *[Export of benchmark results in several formats](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/exportingbenchmarks)* for analysis or visualization
153+
* Running the Instruments profiler [on the benchmark suite executable directly from Xcode](https://github.com/ordo-one/package-benchmark/releases/tag/1.11.0)
154+
155+
### Closing Thoughts
156+
157+
The Swift community, including major public projects like [Swift Foundation](https://github.com/apple/swift-foundation), [SwiftPM](https://github.com/apple/swift-package-manager), [SwiftNIO](https://github.com/apple/swift-nio), and [Google Flatbuffers](https://github.com/google/flatbuffers), has recently embraced the Benchmark package to focus on performance optimization.
158+
159+
Discover how to leverage this tool for your own Swift applications by exploring [the extensive documentation](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark) and join the [conversation on the Swift forums](https://forums.swift.org/c/related-projects/benchmark) to share insights and get answers to your questions. Or why not provide a PR to your favourite open source package that lacks performance tests?
160+
161+
Take the first step to improve your software today, by adding its first benchmark to check performance!
218 KB
Loading

0 commit comments

Comments
 (0)