Static vs Dynamic Dispatch trong Rust

0 0 0

Người đăng: RustDEV VietNam

Theo Viblo Asia

Với Rust, cũng như một số ngôn ngữ lập trình mức hệ thống khác, việc hiểu rõ về các cơ chế phân phối tĩnh (”static dispatch”) và phân phối động (”dynamic dispatch”) là rất quan trọng để có thể có một chương trình tối ưu. Cơ chế “dispatch” có thể hiểu ngắn gọn là cơ chế cho phép chương trình xác định hoặc quyết định được cần phải gọi hàm/phương thức cụ thể nào trong một loạt các hàm/phương thức cùng tên đang cùng tồn tại. Việc này khá quan trọng trong lập trình đa hình (”polymorphism”). Trong Rust, “static dispatch” được thực hiện tại thời điểm biên dịch, còn “dynamic dispatch” sẽ được thực hiện khi chương trình đang thực thi. Bên cạnh nội dung bài viết này bạn cũng có thể xem thêm video hướng dẫn chi tiết hơn về Static vs Dynamic Dispatch trên trang RustDEV Vietnam.

“Static Dispatch” - Tối ưu khi biên dịch

Với cơ chế phân phối tĩnh, các lời gọi hàm sẽ được phân giải và cụ thể hóa thành các mã lệnh của hàm cụ thể được gọi trong quá trình biên dịch chương trình. Trình biên dịch sẽ tạo ra các bản sao mã lệnh của một hàm/phương thức cụ thể cho kiểu dữ liệu cụ thể tại vị trí gọi hàm. Như vậy, dễ thấy rằng nó sẽ tạo nhiều mã máy trong tệp chương trình thi hành và cũng dễ thấy là tốc độ thi hành khi cần sử dụng các hàm này sẽ rất nhanh do chương trình không phải phân giải hoặc tìm kiếm hàm cần thực thi lúc đang hoạt động. Dưới đây là một đoạn chương trình minh họa điển hình.

// Hàm tổng quát sử dụng kỹ thuật phân phối tĩnh
fn process_data<T: Display>(item: T) { println!("Processing: {}", item);
} // Trình biên dịch sẽ sinh ra các mã hàm khác nhau cho từng
// kiểu dữ liệu tại vị trí lời gọi hàm.
fn main() { process_data(42i32); // Sinh mã cụ thể cho process_data với kiểu i32 process_data("hello"); // Sinh mã cụ thể cho process_data với kiểu str
}

Sử dụng “static dispatch” với hàm tổng quát khi chúng ta biết chính xác kiểu dữ liệu tại thời điểm biên dịch và muốn có được tốc độ thực thi tối đa, đặc biệt là trong các tình huống cần tốc độ chi li đến tường nano giây như trên các hệ nhúng.

“Dynamic Dispatch” - Linh hoạt khi thực thi

Phân phối động sử dụng đối tượng thuộc tính hành vi (”trait object”) để “trì hoãn” việc phân giải hàm cần gọi đến khi chương trình hoạt động và dựa vào “runtime” để phân giải. Trình biên dịch sẽ tạo ra một “vtable” (virtual function table - gần giống và linh hoạt hơn cách C++ thực hiện) chứa các con trỏ đến triển khai hàm cụ thể. Tất nhiên, dễ thấy là cách này sẽ ảnh hưởng một chút đến tốc độ do phải phân giải hàm nhưng nó lại cung cấp một khả năng tùy biến cực kỳ linh hoạt. Dưới đây là một đoạn chương trình minh hoạt điển hình.

// Khai báo thuộc tính hành vi
trait Drawable { fn draw(&self);
} struct Circle { radius: f32 }
struct Rectangle { width: f32, height: f32 } impl Drawable for Circle { fn draw(&self) { println!("Drawing circle with radius {}", self.radius); }
} impl Drawable for Rectangle { fn draw(&self) { println!("Drawing {}x{} rectangle", self.width, self.height); }
} // Hàm này tiếp nhận mọi đối tượng có năng lực vẽ hay nói cách khác
// là mọi đối tượng có thuộc tính hành vi Drawable.
fn render_shape(shape: &dyn Drawable) { shape.draw(); // Phân gải tại thời điểm được kích hoạt thi hành
} fn main() { // Một véc-tơ các đối tượng có kiểu dữ liệu bất kỳ, chưa xác định kiểu // cụ thể tại thời điểm biên dịch mà chỉ biết rằng các kiểu dữ liệu đó // có khả năng tự vẽ. let shapes: Vec<Box<dyn Drawable>> = vec![ Box::new(Circle { radius: 5.0 }), Box::new(Rectangle { width: 10.0, height: 20.0 }), ]; // Chưa thể biết phương thức draw() nào sẽ được sử dụng cho đến // khi chương trình thực thi. for shape in shapes { render_shape(&*shape); }
}

Khi nào thì dùng cách nào?

Như trên đã nói, hãy sử dụng static dispatch khi muốn có tố độ tối đa và đã biết chính xác kiểu dữ liệu tại thời điểm biên dịch. Các “game engine”, các thư viện toán học, và các hệ thống thời gian thực rất hay sử dụng kỹ thuật “zero-cost abstraction” này.

Dùng dynamic dispatch khi chúng ta cần một tập hợp các kiểu dữ liệu hỗn tạp, các hệ thống vận hành kiểu “plugin”, hay đơn giản là khi chưa thể xác định được kiểu dữ liệu cụ thể tại thời điểm biên dịch. Các thư viện GUI hoặc các hệ thống vận hành dựa trên sự kiện thường sẽ cần khả năng linh hoạt này.

static dispatch sẽ cần thời gian biên dịch lâu hơn và kích thước chương trình lớn hơn dynamic dispatch nhưng ngược lại dynamic dispatch lại hỗ trợ “polymorphism programming” mạnh mẽ hơn, giải quyết được các vấn đề về linh hoạt mà static dispatch không thể giải quyết được. Thực ra, mặc dù là về mặt lý thuyết thì dynamic dispatch sẽ chậm hơn static dispatch nhưng cũng đã có khá nhiều kiểm nghiệm cho thấy chênh lệch này là không đáng kể so với những gì nhận được.

Để biết thêm chi tiết về nội dung này, hãy xem thêm video hướng dẫn chi tiết tại #28 - Static vs Dynamic Dispatch - Nói thêm một chút trên trang RustDEV Vietnam.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Chuyện cái comment

Chuyện cái comment. Chuyện rằng, có một ông bạn nọ có cái blog ở trên mạng, cũng có dăm.

0 0 38

- vừa được xem lúc

Đừng đánh nhau với borrow checker

Đừng đánh nhau với borrow checker. TL;DR: Đừng bao giờ đánh nhau với borrow checker, nó được sinh ra để bạn phải phục tùng nó .

0 0 33

- vừa được xem lúc

Chuyện biểu diễn ma trận trên máy tính

Chuyện biểu diễn ma trận trên máy tính. Cách đây mấy hôm mình có share cái screenshot trên Facebook, khoe linh tinh vụ mình đang viết lại cái CHIP-8 emulator bằng Rust.

0 0 46

- vừa được xem lúc

Rust và Lập trình Web

Rust và Lập trình Web. Bài viết được dịch lại từ bản gốc "Rust for the Web" đăng tại phiên bản blog tiếng Anh của mình.

0 0 42

- vừa được xem lúc

Viết ứng dụng đọc tin HackerNews bằng Rust

Viết ứng dụng đọc tin HackerNews bằng Rust. Dạo này mình toàn viết bài linh tinh, lâu rồi chưa thấy viết bài kĩ thuật nào mới nên hôm nay mình viết trở lại, mất công các bạn lại bảo mình không biết co

0 0 30

- vừa được xem lúc

Cài đặt Rust trên Arch Linux

Cài đặt Rust trên Arch Linux. Việc cài đặt Rust trên môi trường Arch Linux khá là đơn giản.

0 0 48