Which everyday operations are surprisingly expensive in Swift and iOS code, and how do you find and fix them?
Predict first
Compare a Set with an Array lookup, a rebuilt with a reused DateFormatter, and String.count with utf8.count. Are the cheaper options clearly faster?
import Foundation
func ms(_ f: () -> Void) -> Double {
let t = DispatchTime.now()
f()
return Double(DispatchTime.now().uptimeNanoseconds - t.uptimeNanoseconds) / 1e6
}
let arr = Array(0..<20000), set = Set(arr)
var hits = 0
let slowFind = ms { for i in stride(from: 0, to: 20000, by: 20) { if arr.contains(19999 - i) { hits += 1 } } }
let fastFind = ms { for i in stride(from: 0, to: 20000, by: 20) { if set.contains(19999 - i) { hits += 1 } } }
print("Set faster than Array.contains by more than 20x:", slowFind / fastFind > 20)
let rebuild = ms { for _ in 0..<2000 { let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; _ = f.string(from: Date()) } }
let shared = DateFormatter(); shared.dateFormat = "yyyy-MM-dd"
let reuse = ms { for _ in 0..<2000 { _ = shared.string(from: Date()) } }
print("Reusing a DateFormatter more than 5x faster:", rebuild / reuse > 5)
let text = String(repeating: "é", count: 100_000)
let chars = ms { for _ in 0..<200 { _ = text.count } }
let bytes = ms { for _ in 0..<200 { _ = text.utf8.count } }
print("String.count much slower than utf8.count:", chars / bytes > 10)
- All three print true
- Only the Set comparison prints true
- All three print false, the differences are small
Mental model. It is the difference between looking up a name in a printed phone book once per person you meet, and reading the whole book each time. The work is the same; the count is not.
The same few costs account for most “it is slow” reports. Linear search in a loop: array.contains is O(n), so calling it for every element of another array is O(n²); build a Set or Dictionary once and look up in constant time. Strings: String.count counts Characters (grapheme clusters) by walking the text, so it is O(n) and calling it in a loop condition is quadratic, while utf8.count is O(1) for native strings; integer-offset indexing does not exist for the same reason. Formatters: DateFormatter, NumberFormatter and regex objects are expensive to create (they load locale and calendar data), so create one and reuse it (or the value-type FormatStyle APIs, measuring that they suit your case) instead of building one per cell. Allocation and copying: building big arrays by repeatedly appending without reserveCapacity, and large value types copied through layers, show up as malloc and memmove in Time Profiler.
Two more belong in any profile review. JSON: decoding a large payload is CPU work proportional to its size, so decode on a background context, reuse the JSONDecoder, ask the server for less (fields, pagination), and avoid decoding data you will not show (Q103 compares the untyped route). Images: a decoded bitmap costs width × height × 4 bytes regardless of file size and decoding happens on first draw unless you do it earlier, so downsample and decode off the main thread (Q327). The fix order is the same each time: measure that the operation really is hot (Time Profiler, or XCTest measure), make the algorithm cheaper before micro-tuning, and never leave heavy work on the main thread (Q287).
Each difference is large (the Set lookup by about two orders of magnitude, the formatter reuse by tens of times), so a profile finds them easily.
Common traps
string.countin a loop condition. Each evaluation walks the whole string; cache it, or useisEmptyand index-based iteration.- Creating a
DateFormatterper row. It is one of the most common causes of choppy lists; create it once and reuse it.
They might follow up with
- Why is
String.countnot O(1)? Swift counts user-visible characters, and grapheme clusters can span several scalars. - How do you know an operation is worth optimising? A profile shows it in the heaviest stack, or a measure block shows it exceeds the budget; otherwise leave the clear code alone.
Related questions
- 21.5.2 How do you reduce disk and database cost: batching writes, transactions, atomic writes, memory mapping and…
- 21.2.1 Which Instruments templates do you reach for, how do you read a Time Profiler trace, and where can a sampling…
- 2.5.5 When would you use JSONSerialization instead of Codable, and what does the untyped route cost?
- 12.7.1 How do you design an image loading pipeline: decoding, downsampling, caching and de-duplication?
- 9.7.1 What is a hitch versus a hang, how does ProMotion change the budget, and how do you measure and fix them?
How do you reduce disk and database cost: batching writes… →