Một Số Nhận Định Gần Đây - nói dối e blog

Một Số Nhận Định Gần Đây

Gần đây tôi có vài suy ngẫm đáng để chia sẻ. Khoảng thời gian vừa qua công việc đặc biệt bận rộn đến mức mỗi ngày đều không đủ giờ để hoàn thành công việc lập trình. Có những vấn đề khi chưa hoàn tất tôi không muốn công khai thảo luận, nên chỉ đăng một số ghi chú ngắn vào bản báo cáo tuần của công ty. Khi giai đoạn này qua đi tôi sẽ chuyển chúng sang đây.

Tuy nhiên vẫn có vài kinh nghiệm tổng quát có thể chia sẻ. Cách đây vài ngày tôi gặp phải bài toán tối ưu hóa. Tôi muốn áp dụng phương pháp xây dựng bản đồ đường đi định kỳ để tối ưu thuật toán tìm đường, thay vì mỗi lần đơn vị tìm mục tiêu đều phải thực hiện tính toán riêng lẻ và ghi lại kết quả đường đi như trước đây. Mọi thứ dường như đều tiến triển thuận lợi, tính đúng đắn của thuật toán nhanh chóng được xác nhận. Tuy nhiên khi tiến hành chạy thử thực tế, chỗ tạo bản đồ đường đi lại xuất hiện hiện tượng giật nhẹ ảnh hưởng đến trải nghiệm mượt mà.

Khi gặp tình huống như thế, phản ứng đầu tiên thường là suy nghĩ cách tối ưu hóa. Chẳng hạn như có nên chuyển sang sử dụng đa luồng xử lý, có cần phân bổ lại khối lượng tính toán, hay có thể đơn giản hóa vấn đề để giảm số lần tính toán hay không.

Tôi đã dành vài giờ đồng hồ để thử nghiệm các giải pháp trên. Kết quả cho thấy nếu đơn giản thêm tùy chọn tối ưu O3 thì hiệu suất có thể tăng gấp 2 lần, ngang bằng với hiệu quả cải tiến từ những nỗ lực tối ưu khác (dĩ nhiên những cách kia cũng mang lại hiệu quả rõ rệt).

Điều này không hoàn toàn phù hợp với kinh nghiệm thường thấy của tôi. Trong đa số trường hợp, với chương trình viết bằng ngôn ngữ C, việc tối ưu hóa của trình biên dịch có thể mang lại cải thiện hiệu suất khoảng 25% đã là rất tốt (thông thường mã code theo phong cách C++ cho phép cải thiện nhiều hơn). Có lẽ lần này là do đặc thù của đoạn mã thuần thuật toán.

Tuy nhiên, việc cải tiến chương trình bằng cách áp dụng đa luồng xử lý lại không phù hợp với trực giác của tôi. Tôi không cho rằng biến đổi cấu trúc chương trình thành phức tạp hơn chỉ để đạt được hiệu năng mong muốn là lựa chọn tối ưu. Hơn nữa, khi thiết kế thuật toán ban đầu, tôi đã từng ước lượng rằng với môi trường phần cứng hiện tại, sẽ không xuất hiện độ trễ lớn đến mức không thể chấp nhận được.

Sau cùng tôi tự kiểm tra lại đoạn code mình đã viết. Hóa ra tôi đã vô tình triển khai một thuật toán có độ phức tạp O(n) thành O(n²). Sau khi sửa lỗi này, tốc độ xử lý lập tức tăng lên 100 lần.

Đây là một bài học đáng nhớ và hiếm gặp. Mặc dù kết quả chương trình vẫn đúng, nhưng vẫn tồn tại lỗi phần mềm. Chỉ là lỗi này không ảnh hưởng đến tính chính xác mà chỉ gây ra chậm trễ – biến một nhiệm vụ chỉ nên mất 1/1000 giây thành việc cần đến 1/10 giây để hoàn thành.

Một điểm cảm nhận khác:

Trong những bài toán có điều kiện bên ngoài ít biến động, việc sử dụng thích hợp các hằng số rất hiệu quả. Chẳng hạn như xác định độ dài chuỗi không vượt quá 64 ký tự, số phần tử mảng tối đa 16 phần tử…

Trong code phong cách C, chúng ta thường thấy các macro MAXxxx được dùng để định nghĩa kích thước mảng trực tiếp. Trong các tài liệu hướng dẫn C++ thì đây thường được coi là ví dụ tiêu cực, khuyên nên thay thế bằng std::vector.

Thực tế, nếu có thể đánh giá chính xác bài toán mục tiêu và đưa ra các khẳng định hợp lý trong code, thì việc dùng mảng có độ dài cố định mang lại nhiều lợi ích. Cấu trúc dữ liệu đơn giản hơn giúp chương trình ổn định hơn, đặc biệt thể hiện rõ ưu điểm trong môi trường đa luồng xử lý.

Ngay cả những phần cấp phát động, nếu có thể ước tính được dung lượng tổng thể, thì việc định nghĩa sẵn bộ nhớ tạm thời (memory pool) trong cấu trúc sẽ gọn gàng hơn nhiều. Nghĩa là không cần dùng malloc để xin cấp phát một khối nhớ mới rồi trỏ đến qua con trỏ, mà thay vào đó trực tiếp khai báo khối nhớ trong cấu trúc, để con trỏ động trong cấu trúc tham chiếu đến các vị trí trong bộ nhớ tạm của chính nó.

Như vậy, bố cục dữ liệu của các đối tượng phức tạp vẫn giữ được tính liên tục trong bộ nhớ. Chi phí quản lý vòng đời của chúng cũng thấp hơn, hỗ trợ đa luồng xử lý đơn giản hơn. Tuy nhiên, phương pháp này đòi hỏi phải có đánh giá rõ ràng về nhu cầu sử dụng, phù hợp với các module có tính kết dính cao được định nghĩa chặt chẽ.

Gần đây tôi đọc một cuốn sách về Objective-C, trong đó có phần gây ấn tượng sâu sắc là cơ chế quản lý bộ nhớ của framework Cocoa.

Dù Cocoa cũng sử dụng kỹ thuật đếm tham chiếu, nhưng không giải phóng bộ nhớ ngay khi đếm về 0. Thay vào đó kết hợp với cơ chế autorelease pool.

Cách làm này giúp giảm thiểu lỗi truy cập đối tượng vô hiệu trong stack frame hiện tại, mà không cần áp dụng mô hình RAII như C++. Đồng thời thiết lập quy tắc quản lý bộ nhớ rõ ràng, dễ hiểu hơn.

0%