Điều phối phương thức
Một lời gọi rốt cuộc đi về đâu, và tìm ra chỗ đó tốn gì
Mọi lời gọi phương thức đều phải trả lời một câu hỏi: đoạn code nào sẽ chạy? Swift có ba câu trả lời, và bạn nhận được cái nào là do nơi phương thức được khai báo quyết định — không phải do cách nó được gọi.
Điều phối tĩnh. Địa chỉ đã biết ngay lúc biên dịch. Lời gọi là một cú nhảy trực tiếp, và trình tối ưu có thể nội tuyến nó đi hẳn.
Điều phối bảng. Đối tượng mang theo một bảng con trỏ hàm; lời gọi đọc ô tương ứng rồi nhảy. Một lớp gián tiếp, và không nội tuyến qua được nó.
Điều phối thông điệp. objc_msgSend của Objective-C duyệt cây phân cấp lớp lúc chạy để tìm một
selector. Chậm nhất, và hoàn toàn động — phần cài đặt có thể bị tráo trong lúc chương trình đang
chạy.
Cái nào nhận cái gì
| Khai báo trong | Điều phối |
|---|---|
struct / enum |
Tĩnh, luôn luôn |
final class |
Tĩnh |
class, phương thức không final |
Bảng (vtable) |
| Yêu cầu của protocol | Bảng (witness table) |
| Extension của protocol, không phải yêu cầu | Tĩnh |
| Extension của class | Tĩnh |
@objc dynamic |
Thông điệp |
Hai dòng trong bảng đó gây ra phần lớn những bất ngờ.
Cái bẫy extension của protocol
Một phương thức trong extension của protocol mà không được khai báo trong chính protocol sẽ được điều phối tĩnh, dựa trên kiểu tĩnh của biến. Điều đó sinh ra hành vi thật sự phản trực giác:
protocol Greeter {
func hello() // một yêu cầu
}
extension Greeter {
func hello() { print("protocol hello") }
func goodbye() { print("protocol goodbye") } // KHÔNG phải yêu cầu
}
struct English: Greeter {
func hello() { print("English hello") }
func goodbye() { print("English goodbye") }
}
let direct = English()
direct.hello() // English hello
direct.goodbye() // English goodbye
let viaProtocol: Greeter = English()
viaProtocol.hello() // English hello — witness table
viaProtocol.goodbye() // protocol goodbye — tĩnh, không có ô trong witness table
goodbye() không có ô nào trong witness table, vì protocol chưa bao giờ khai báo nó. Trình biên dịch
giải nó dựa trên kiểu duy nhất nó biết: Greeter.
Cảnh báo
Đây không phải lỗi và cũng sẽ không thay đổi. Quy tắc để làm việc: nếu một phương thức được định cho các kiểu tuân thủ ghi đè lại, hãy khai báo nó trong protocol. Một extension không có yêu cầu tương ứng chỉ là phần cài đặt mặc định cho những kiểu không tự cung cấp bản của mình — và chỉ khi được gọi qua một kiểu cụ thể.
final không chỉ là tài liệu
Đánh dấu một class hay một phương thức là final chuyển nó từ điều phối bảng sang điều phối tĩnh,
đó là một tối ưu thật và đôi khi là một tối ưu lớn trong một vòng lặp nóng. Quan trọng hơn, nó cho
phép trình tối ưu nội tuyến, thứ mở khóa mọi tối ưu phía sau việc nội tuyến.
final class Renderer {
func draw() { … } // tĩnh — gọi trực tiếp, nội tuyến được
}
Đôi khi trình biên dịch tự suy ra được điều này. Whole Module Optimization cho phép nó thấy rằng một class không có lớp con nào trong module và khử ảo hóa lời gọi. Việc này chỉ chạy được trong phạm vi một module, nên một class public trong một framework vẫn bị điều phối bảng dù có làm gì đi nữa — trình biên dịch không thể biết module khác sẽ kế thừa cái gì.
Đó là lý do đằng sau các quy tắc truy cập mặc định của Swift: class và phương thức trong một package
hay framework không mở cho việc kế thừa trừ khi bạn viết open, chính là để trình tối ưu giữ được
quyền tự do của nó.
Chỗ @objc dynamic là bắt buộc
Điều phối thông điệp là chậm nhất, và có những trường hợp cần đến nó:
class ViewModel: NSObject {
@objc dynamic var progress: Double = 0
}
KVO đòi nó. Key-value observing hoạt động bằng cách tráo setter lúc chạy, thứ chỉ khả thi với
điều phối thông điệp. Một thuộc tính dynamic là cái giá của việc quan sát được theo cách này, và đó
cũng là chỗ @Published khác đi — Combine đạt được việc quan sát mà không cần runtime Objective-C.
Method swizzling đòi nó. Bất cứ thứ gì thay thế một phần cài đặt lúc chạy — vài SDK phân tích, vài công cụ kiểm thử — đều cần điều phối thông điệp để có cái mà thay.
Vài khuôn mẫu UIKit đòi nó. Các đích của #selector, và bất cứ thứ gì framework tra cứu theo tên
thay vì theo con trỏ.
Ngoài những trường hợp đó, @objc dynamic là cái giá không đi kèm lợi ích nào.
Chuyện này thật ra tốn bao nhiêu
Đáng giữ cho đúng tỷ lệ. Một lần điều phối bảng đại khái là một lần đọc bộ nhớ thêm và một cú nhảy gián tiếp — vài nano giây, và chẳng ăn thua gì với bất cứ thứ gì động đến mạng, đĩa hay màn hình. Nó quan trọng ở hai chỗ: bên trong một vòng lặp chặt chạy hàng triệu lần, và ở chỗ nó chặn việc nội tuyến mà lẽ ra đã mở đường cho các tối ưu khác.
Cái thứ hai thường mới là tác động lớn hơn. Một lời gọi tĩnh được nội tuyến cho phép trình tối ưu nhìn xuyên qua nó — gấp hằng số, loại bỏ các phép kiểm tra biên, giữ giá trị trong thanh ghi. Một lần điều phối bảng là một bức tường mờ đục.
Nên lời khuyên thực dụng không phải “tránh dùng class”. Mà là:
- Hãy đánh dấu class là
finaltrừ khi nó được thiết kế để kế thừa. Việc này miễn phí và diễn đạt đúng ý định. - Hãy khai báo các yêu cầu của protocol trong chính protocol, không chỉ trong extension.
- Chỉ với tới
@objc dynamickhi tính năng runtime thật sự cần đến nó. - Hãy đo trước khi tái cấu trúc bất cứ thứ gì vì điều phối. Trong code ứng dụng, câu trả lời gần như luôn là điều phối không phải vấn đề.