XPerf
LanguagesSwift

Swift performance guide (v0)

Fewer force-unwraps on hot paths, fewer quadratic string builders.

Scope: Swift 5.9+. Speedup claims cite a local bench, an upstream source, or are marked provisional. Corpus residual counts are lint hit counts, not wall-time savings.

Profile first (Instruments, swift package benchmark when available). ARC and specialization change costs — re-bench after toolchain bumps.

Contents

1. Optionals and hot paths

Force-unwrap (!) in a tight loop crashes on nil and skips safer control flow. Prefer guard let / if let once per iteration or hoist.

Force-unwrap and if let took the same time on a non-nil array, so there is no lint for it.

2. Strings, collections, and printing

Repeated s += piece can be costly; prefer reserved capacity / builders when measured. Hot print is I/O-bound — gate it. Appending to an empty Array in a sized loop reallocates as it grows; call reserveCapacity first when the final count is known.

Lint: sp-print-hot (default off; about 10× a String when stdout is discarded); sp-array-no-reserve (default hint; about 5× on many small arrays, about 1.6× on one large fill). String += with reserveCapacity was slower, and map then joined was about 1.2×, so those lints were removed.

3. Formatters, JSON, and regular expressions

DateFormatter, JSONDecoder, and NSRegularExpression do heavy setup. Building one on every iteration of a hot loop repeats that work; create it once and reuse it. JSONSerialization.jsonObject belongs with the JSON case. A Swift Regex value is a different API and is not flagged. These lints are hint. Each has a JSONL microbench and stays hint. They are not part of the 2026-10-22 corpus-ready verdict.

String(contentsOfFile:) of one path was about 414× versus reading once. A path that changes every iteration cannot be hoisted.

Date() every iteration was about 244× versus one sample. A loop that needs a fresh timestamp has nothing to hoist. DateFormatter stays its own rule.

value(forKey:) every iteration was about 35× versus reading the property. A name that changes every iteration cannot become a direct property. This is the same family as Go FieldByName.

Lint: sp-dateformatter-hot, sp-json-decode-hot, sp-regex-hot, sp-file-read-hot, sp-clock-hot, sp-value-for-key-hot (default hint).

Lint map

IDSmellDefault
sp-print-hotprint( in loopoff (about 10× a String; stdout discarded)
sp-array-no-reserveempty Array then append without reserveCapacityhint (about 5× small, about 1.6× one large fill)
sp-dateformatter-hotdate formatter constructed in a loophint (JSONL present; not corpus-triaged)
sp-json-decode-hotJSON decoder or jsonObject in a loophint (JSONL present; not corpus-triaged)
sp-regex-hotregular expression constructed in a loophint (JSONL present; not corpus-triaged)

Enable an off rule with --severity <rule>=hint. The five rules from 2026-10-22 still have no JSONL. The three rows added 2026-10-23 have a JSONL microbench and stay hint. They are not part of the 2026-10-22 corpus-ready verdict. See evidence-policy.md.

References