Trang chủ

Hiệu năng

Đ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ò.

Ba kiểu điều phối lời gọi

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 ở đó.

  • Tĩnh — trình biên dịch biết chính xác hàm nào. Struct, class final, phương thức private, phương thức trong extension của protocol mà không phải yêu cầu.
  • Bảng — một lần gián tiếp qua vtable (class) hoặc bảng nhân chứng (yêu cầu của protocol). Rẻ, nhưng mờ đục: trình tối ưu không nhìn xuyên qua được.
  • Thông điệpobjc_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

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:

  • Thực thể 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ì.
  • Một existential (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.

Đặt trước dung lượ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ì.

Hãy đo, và đo đúng bản build

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.

Một danh sách kiểm tra ngắn

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:

  1. Bạn có đang đo trên bản release không?
  2. Phần việc đó có đang chạy nhiều lần hơn bạn tưởng không? (Hãy log số lần gọi trước — thắng lợi về mặt thuật toán làm lu mờ mọi thứ khác ở đây.)
  3. Có kiểu nào ở đường nóng đang là class mà lẽ ra nên là struct không?
  4. Các class ở đường nóng đã final chưa?
  5. Có lời gọi protocol nóng nào đang là any mà lẽ ra nên là generic hoặc enum không?
  6. Các mảng đã đặt trước dung lượng chưa?
  7. Một chuỗi 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?