Điều phối lời gọi, cấp phát, và đo thay vì đoán
Mọi chương trước đều kết lại ở một cái giá. Chương này nói về việc nhìn thấy những cái giá đó thay vì đoán mò.
Cách một lời gọi phương thức được phân giải quyết định nó có nội tuyến được hay không, mà nội tuyến là nơi phần lớn tốc độ nằm ở đó.
final, phương thức private,
phương thức trong extension của protocol mà không phải yêu cầu.objc_msgSend của Objective-C, dùng cho thành viên @objc dynamic và mọi thứ
thừa kế từ NSObject. Linh hoạt nhất và chậm nhất, cách biệt rất xa.Bạn đẩy được một lời gọi từ bảng về tĩnh bằng ba chú thích chẳng tốn gì:
final class Renderer { // không có lớp con → khử ảo hoá
private func prepare() {} // không lộ ra ngoài → khử ảo hoá
}
và bằng cách build với tối ưu hoá toàn module, thứ cho phép trình biên dịch chứng minh rằng một
class internal không có lớp con nào trong module. Đó là mặc định của bản release; lý do bản debug
có thể chậm gấp mười lần phần lớn nằm ở đây.
Cấp phát trên heap là cái giá lớn thứ hai, và dễ nhận ra hơn cơ chế điều phối vì nó đi theo cấu trúc kiểu:
class luôn nằm trên heap.struct nằm nội tuyến — trong thanh ghi, trên stack, hoặc bên trong thứ chứa nó — trừ khi bị một
closure thoát ly bắt giữ, hoặc bị đóng hộp vào existential lớn hơn ba từ máy.Array, String, Dictionary, Set cấp phát vùng đệm ngay khi chứa bất cứ thứ gì.any P) lớn hơn ba từ máy sẽ cấp phát một hộp.protocol Shape { var area: Double { get } }
struct Circle: Shape { // 8 byte — vừa nội tuyến trong existential
var radius: Double
var area: Double { .pi * radius * radius }
}
struct Rect: Shape { // 32 byte — bị đóng hộp khi lưu dưới dạng `any Shape`
var x, y, width, height: Double
var area: Double { width * height }
}
Hai kiểu trông tương tự nhau, mà một trong hai âm thầm cấp phát mỗi lần được cất vào [any Shape].
Nếu điều đó quan trọng, hãy dùng enum thay cho protocol — một enum có giá trị đi kèm là một giá trị
phẳng, kích thước bằng case lớn nhất, không cấp phát và không điều phối động.
Nối thêm vào mảng làm vùng đệm lớn lên theo cấp số nhân, mỗi lần lớn là một lần sao chép:
var result: [Transform] = []
result.reserveCapacity(items.count)
for item in items { result.append(transform(item)) }
Một lần cấp phát thay cho mấy vòng cấp phát – sao chép – giải phóng. Đáng làm bất cứ khi nào biết trước kích thước cuối, mà viết ra thì chẳng tốn gì.
Sai lầm hiệu năng phổ biến nhất trong Swift là đo trên bản debug. Không có tối ưu hoá, generic không được chuyên biệt hoá, chẳng gì được nội tuyến, lưu lượng ARC ở mức tệ nhất, và kiểm tra biên nằm khắp nơi. Con số từ bản build đó không nói gì về bản bạn phát hành.
import Testing
@Test func decodingStaysFast() async throws {
let payload = try fixture(named: "large-feed")
let clock = ContinuousClock()
let elapsed = clock.measure {
for _ in 0..<100 { _ = try? JSONDecoder().decode(Feed.self, from: payload) }
}
#expect(elapsed < .milliseconds(500))
}
Một bài kiểm thử như vậy ghim con số lại, để một bước lùi hiệu năng lộ ra ở CI chứ không phải ở một
bình luận review. Còn để tìm xem thời gian đi đâu, Time Profiler trong Instruments vẫn là công cụ;
hai thứ đáng biết là cờ -Xswiftc -O khi build và Signpost (OSSignposter) để tự đánh dấu các
khoảng đo của mình.
Mẹo
Thứ tự thao tác: làm cho đúng đã, rồi đo, rồi sửa mục đứng đầu, rồi đo lại. Mọi tối ưu hoá trong chương này đều đánh đổi bằng khả năng đọc hiểu, mà phần lớn chương trình tiêu tốn thời gian ở một hai chỗ chẳng ai đoán ra.
Khi thứ gì đó chậm hơn mức đáng ra phải có, theo thứ tự thường đem lại kết quả nhất:
class mà lẽ ra nên là struct không?final chưa?any mà lẽ ra nên là generic hoặc enum không?lazy — hoặc việc thiếu nó — có đang cấp phát các mảng trung gian mà bạn không bao giờ
đọc tới không?