Trongchương này, các tác giả trình bày một số vấn đề về tối ưu hóa xử lý chươngtrình, trong đó tập trung đi sâu phân tích các khía cạnh về tối ưu hóa thời gian xử lý, đồng thời trình bày
Trang 1BÁO CÁO MÔN HỌC
NGUYÊN LÝ VÀ PHƯƠNG PHÁP LẬP TRÌNH
MỘT SỐ VẤN ĐỀ VỀ TỐI ƯU HÓA XỬ LÝ CỦA CHƯƠNG TRÌNH
Cán bộ giảng dạy: TS Nguyễn Tuấn Đăng
Học viên: Nguyễn Bá Kỳ
Mã số: CH0901025
Trang 2Chúng em xin chân thành cám ơn TS Nguyễn Tuấn Đăng đã tận tình giảng dạy chúng em trong quá trình học tập để thực hiện bài tiểu luận này Chúng em xin chân thành cám ơn Phòng Đào tạo sau Đại học, trường Đại Học Khoa Học Công nghệ Thông tin – Đại học Quốc Gia Tp.HCM đã tạo điều kiện thuận lợi cho em trong học tập và công tác
Chúng em xin chân thành cám ơn quý Thầy Cô trong Trường đã tận tình giảng dạy, trang bị cho chúng em những kiến thức quý báu trong suốt thời gian học
Xin chân thành cám ơn người thân và bạn bè đã ủng hộ, giúp đỡ tôi trong thời gian học tập và nghiên cứu
Học viên thực hiện, Nguyễn Bá Kỳ
Tháng 10/ 2010
Trang 31.0 Đặt vấn đề 5
1.1 Hiện tượng nút cổ chai 7
1.2 Kỹ thuật đo thời gian thực hiện và lưu lại quá trình hoạt động (Profiling) 15
1.2.1 Đo thời gian tự động 15
1.2.2 Kỹ thuật profiling – lưu lại quá trình thực hiện 17
1.2.3 Tập trung quan tâm vào các đoạn mã trọng tâm của chương trình 19 1.2.4 Biểu diễn trực quan số liệu thống kê hiệu năng thực hiện của chương trình 21
1.3 Các chiến lược tối ưu hóa thời gian thực hiện 23
1.3.1 Sử dụng thuật toán hoặc cấu trúc dữ liệu tốt hơn 23
1.3.2 Sử dụng các chức năng tối ưu hóa của trình biên dịch 23
1.3.3 Tinh chỉnh mã nguồn 25
1.3.4 Không tối ưu hóa những gì không gây vấn đề 26
1.4 Tinh chỉnh mã nguồn 28
1.4.1 Tập hợp những biểu thức chung 28
1.4.2 Thay thế các thao tác chi phí cao bằng các thao tác chi phí thấp hơn 29
1.4.3 Trải rộng hay loại bỏ các vòng lặp 30
1.4.4 Lưu trữ lại các giá trị thường dùng 30
1.4.5 Xây dựng chức năng cấp phát bộ nhớ đặc dụng 31
1.4.6 Dùng bộ nhớ trung gian cho việc nhập và xuất dữ liệu 32
1.4.7 Xử lý riêng từng tình huống đặc biệt 33
1.4.8 Tính trước kết quả 33
1.4.9 Dùng các giá trị xấp xỉ 34
1.4.10 Viết lại mã nguồn bằng ngôn ngữ cấp thấp 34
1.5 Tối ưu hóa không gian lưu trữ 35
1.5.1 Tiết kiệm không gian bằng cách dùng kiểu dữ kiệu có kích thước nhỏ nhất 36
1.5.2 Không cần lưu trữ những gì có thể tính lại dễ dàng 37
1.6 Ước lượng 39
1.7 Tổng kết 43
1.8 Các tài liệu đọc thêm 44
Bài tập 1.1 – Chương trình Spam Test 45
1 Mục đích 45
2 Các chức năng của chương trình 46
3 Cấu trúc dữ liệu và giải thuật được sử dụng 46
4 Nhận xét 48
Bài tập 1.2 1 Yêu cầu 49
2 Thực hiện 49
Trang 42 Giải thích 50
Bài tập 1.4 - Module Special Memset 51
1 Mục đích 51
2 Giải thích hoạt động của module Special Memset 51
Bài tập 1.5 - Module Special Allocator 53
1 Mục đích 53
2 Giải thích hoạt động của module Special Allocator 53
Bài tập 1.6 và 1.7 - Chương trình Cost Model 56
1 Mục đích 56
2 Các chức năng của chương trình 57
Trang 5chuỗi ký tự mẫu 14Hình 1.2 Đồ thị thể hiện sự biến thiên tốc độ xử lý của chương trình với kíchthước của bảng băm trong trường hợp kích thước là lũy thừa của 2 và trường hợp
Danh sách các bảngBảng 1.1 Bảng thống kê quá trình thực hiện chương trình spamtest nguyên thủy
19Bảng 1.2 Bảng thống kê quá trình thực hiện chương trình spamtest cải tiến 20Bảng 1.3 Kết quả đo tốc độ thực hiện của các lệnh cơ bản trong C/C++ trên hệthống Intel Pentium IV 2.4 GHz, 512MB DDRAM 333 Mhz 58
Trang 6Chương 1 Một số vấn đề về Tối ưu hóa xử lý chương trình
Bài viết này dựa trên nội dung của chương 7 “Hiệu năng xử lý của chươngtrình” (Chapter 7 – Performance) trích trong sách “The Practice ofProgramming” của tác giả Brian W Kernighan va Rob Pike [1999] Trongchương này, các tác giả trình bày một số vấn đề về tối ưu hóa xử lý chươngtrình, trong đó tập trung đi sâu phân tích các khía cạnh về tối ưu hóa thời gian
xử lý, đồng thời trình bày một số kỹ thuật về tối ưu hóa không gian lưu trữ.1.0 Đặt vấn đề
Vấn đề tối ưu hóa xử lý của chương trình được đặt ra từ rất lâu rồi Trướcđây, các nhà lập trình luôn cố gắng tối ưu hóa chương trình của mình do các thế
hệ máy tính bấy giờ có tốc độ xử lý còn chậm và giá thành cao Ngày nay, máytính đã trở nên phổ biến và tốc độ xử lý ngày càng tăng nhanh, do đó nhu cầuđặt ra đối với bài toán tối ưu hóa tuyệt đối chương trình đã giảm đi rất nhiều.Vậy ta có còn cần quan tâm đến vấn đề hiệu năng xử lý của chương trình nữahay không?
Trên thực tế, bài toán tối ưu hóa hiệu năng xử lý của chương trình vẫn rất có
ý nghĩa trong cuộc sống Tuy nhiên, vấn đề này chỉ thật sự cần được đặc biệtquan tâm khi bài toán cần được giải quyết thật sự có ý nghĩa quan trọng, trongkhi chương trình hiện có để giải quyết bài toán này có tốc độ xử lý còn quáchậm so với yêu cầu của thực tế, đồng thời ta có cơ sở để tối ưu hóa việc xử lýnhằm tăng tốc độ của chương trình mà vẫn đảm bảo tính đúng đắn, bền vững vàtrong sáng của chương trình Một chương trình có tốc độ nhanh nhưng đưa rađáp số chưa chính xác không giúp tiết kiệm được thời gian!
Do đó, nguyên lý đầu tiên của tối ưu hóa là phải cân nhắc có cần tối ưu hóachương trình hay không Phải chăng chương trình hiện có đã đủ tốt rồi, không
Trang 7cần hoặc không thể tối ưu hóa thêm được nữa? Giả sử ta đã biết được cách màmột chương trình sẽ được sử dụng và môi trường thực hiện chương trình này,liệu việc tối ưu hóa nhằm tăng tốc độ xử lý của chương trình có đem lại lợi ích
gì hay không Hầu hết những chương trình mà sinh viên viết trong quá trình học
ở trường chỉ được sử dụng một vài lần, sau đó sẽ không sử dụng lại nữa, do đó,vấn đề tốc độ thường không được quan tâm Vấn đề này thường cũng khôngảnh hưởng và không cần quan tâm đối với hầu hềt các chương trình cá nhân,chẳng hạn như các công cụ, khung kiểm tra, thử nghiệm Ngược lại, đối vớicác chức năng xử lý chính của một sản phẩm thương mại, chẳng hạn như thưviện đồ họa của phần mềm, vấn đề tối ưu hóa tốc độ xử lý có ý nghĩa hết sứcquan trọng và cần được giải quyết
Như vậy, khi nào thì ta nên thử tìm cách tăng tốc xử lý của một chương trình
và làm cách nào để thực hiện được điều này? Kết quả cụ thể mà ta mong muốnkhi tiến hành việc tối ưu hóa là gì?
Nội dung của chương này sẽ trình bày các vấn đề về làm thế nào để chươngtrình chạy nhanh hơn (tối ưu hóa thời gian) hay sử dụng bộ nhớ tiết kiệm hơn(tối ưu hóa không gian lưu trữ) Tốc độ là vấn đề thường được quan tâm nhiềuhơn nên ta sẽ đi sâu vào vấn đề này Không gian lưu trữ (bộ nhớ chính, đĩa)thường ít được quan tâm hơn, nhưng cũng có ý nghĩa hết sức quan trọng, nên tacũng sẽ tìm hiểu một số vấn đề liên quan đến tối ưu hóa không gian lưu trữ.Trong chương 2 - Thuật toán và Cấu trúc Dữ liệu, chiến lược tốt nhất là sửdụng các thuật toán đơn giản nhất, rõ ràng nhất, và các cấu trúc dữ liệu phù hợpvới bài toán cần giải quyết Do đo, cần đo các thông số hiệu năng hoạt động đểquyết định có cần tiến hành sửa đổi chương trình/thuật toán hay không; cần sửdụng các option của trình biên dịch để phát sinh mã thực hiện có tốc độ xử lýnhanh nhất; cần xác định nhũng phần nào trong chương trình khi được tối ưuhóa sẽ đem lại hiệu quả cao nhất cho toàn bộ chương trình; thực hiện lần lượttừng thay đổi cải tiến rồi sau đó đánh giá, phân tích trước khi tiến hành các thayđổi tiếp theo; đồng thời cần phải giữ lại phiên bản đơn giản chính xác (chưa tối
Trang 8ưu hóa nhưng chắc chắn chính xác) để kiểm tra kết quả so với các phiên bản cảitiến.
Việc đo lường và thống kê là thành phần quan trọng của quy trình cải tiếnhiệu năng của chương trình vì suy luận và trực giác của con người có thể dẫnđến những định hướng sai lầm và cần phải được bổ sung bằng những công cụ
đo thời gian thực hiện và lập sơ đồ thời gian của chương trình Dựa vào các sốliệu đo đạt và thống kê được thông qua các kỹ thuật như đo thời gian (timingcommand) và lưu lại quá trình hoạt động (profiler), ta có thể rút ra được nhữngnhận xét và giải pháp nhằm tối ưu hóa chương trình Việc cải tiến hiệu năngphải đi kèm với quá trình kiểm chứng chương trình (sử dụng kỹ thuật kiểm tra
tự động, lưu vết các sửa đổi và phiên bản, thanh tra mã nguồn – code inspection,
sử dụng các bộ dữ liệu thử nghiệm ) để đảm bảo chương trình sau khi sửa đổivẫn đảm bảo tính đúng đắn và không làm mất đi các ưu điểm vốn có trong cácphiên bản trước đó
Nếu từ ban đầu chương trình được viết tốt với thuật toán hiệu quả thì có thểchỉ cần sửa đổi rất ít hoặc thậm chí không cần sửa đổi chương trình để cải tiếnhiệu năng Ngược lại, đối với những chương trình viết chưa tốt, tổ chức chưahợp lý, không trong sáng, việc tối ưu hóa đòi hỏi phải sửa đổi rất nhiều, thậmchí phải tổ chức lại cấu trúc chương trình và viết lại từ đầu
1.1 Hiện tượng nút cổ chai
Đầu tiên, ta mô tả cách giải quyết hiện tượng nút cổ chai trong một chươngtrình quan trọng được sử dụng thường xuyên trong môi trường cục bộ
Các thư điện tử mà ta nhận được thông qua thiết bị gateway nối kết giữamạng cục bộ với Internet Mỗi ngày, có hàng vạn thư điện tử từ bên ngoài đượcgởi đến cho vài nghìn thành viên trong mạng cục bộ thông qua gateway và sau
đó được chuyển đến từng người trong mạng nội bộ Sự ngăn cách này cô lậpmạng nội bộ của ta với Internet và cho phép ta chỉ cần công bố một tên máy (làtên của gateway) cho tất cả mọi thành viên trong mạng nội bộ
Trang 9Một trong số các dịch vụ của gateway là lọc các “spam”, là các thư rác kèm
theo các quảng cáo về những dịch vụ mà lợi ích chưa được kiểm chứng Sau
thời gian thử nghiệm ban đầu thành công, bộ lọc các “spam” được chính thức
cài đặt và sử dụng như một đặc tính không thể thiếu cho tất cả người dùng trongmạng nội bộ, và rồi rắc rối lập tức xuất hiện Gateway, vốn cũ kỹ và thường rấtbận, bị quá tải vì chương trình lọc chiếm quá nhiều thời gian – nhiều hơn hẳn sovới thời gian cần thiết cho tất cả các thao tác xử lý khác đối với bức thư – đếnmức độ hàng đợi thư bị đầy và việc truyền phát thư tín bị trì hoãn nhiều giờtrong khi hệ thống cố xoay sở để kịp phân phối thư
Trên đây là một ví dụ về vấn đề tốc độ thực hiện chương trình: chương trìnhkhông đủ nhanh để hoàn thành nhiệm vụ, và sự chậm trễ này tạo ra trở ngại chongười dùng Do đó, chương trình cần phải được cải tiến, tối ưu hóa để có thểchạy nhanh hơn nhiều so với phiên bản hiện tại
Trước hết, ta hãy khảo sát cách hoạt động của bộ lọc “spam” Mỗi thông
điệp gửi tới được xem như một chuỗi ký tự đơn, và một bộ so mẫu ký tự kiểmtra chuỗi đó xem có chứa bất kỳ một cụm từ nào trong số các cụm từ thườnggặp trong các “spam” hay không, chẳng hạn như: “Make millions in your sparetime” (kiếm hàng triệu đồng trong thời gian rảnh của bạn) hay “XXX-rated”(các trang khiêu dâm) Các thông điệp có khuynh hướng lặp đi lặp lại, do đó, kỹ
thuật này có hiệu quả đáng kể, và nếu như một thông điệp “spam” lọt lưới thì một cụm từ đặc trưng cho “spam” đó sẽ được thêm vào danh sách các “spam”
để chặn nó vào lần sau
Do không có sẵn một công cụ so sánh chuỗi vừa chạy nhanh vừa đáng tin
cậy, ví dụ như grep, nên người ta viết ra một bộ lọc riêng dùng cho mục đích lọc các “spam” Mã nguồn rất đơn giản; chương trình lọc “spam” sẽ tìm xem
mỗi bức thông điệp có chứa một trong số các cụm từ (các mẫu) nào đó haykhông:
/* Hàm isspam:
kiểm tra chuỗi thông điệp mesg có chứa spam nào không */
Trang 10int isspam(char *mesg)
{
int i;
for (i=0; i<npat; i++)
if (strstr(mesg, pat[i]) != NULL){
printf("spam: match for '%s'\n", pat[i]);}
}
Làm thế nào ta có thể thực hiện đoạn chương trình trên nhanh hơn? Ta cầnphải tìm kiếm chuỗi ký tự mẫu trong bức thông điệp, do đó, việc sử dụng hàmstrstr của thư viện C là cách tốt nhất để tìm kiếm vì đây là hàm thư việnchuẩn được xây dựng hiệu quả
Sử dụng kỹ thuật lưu lại quá trình hoạt động (kỹ thuật profiling - sẽ đượctrình bày chi tiết trong phần 1.2.2-Kỹ thuật profiling – lưu lại quá trình thựchiện.), ta nhận thấy rằng hàm strstr có một số vấn đề chưa phù hợp khi sử
dụng cho một bộ lọc spam Trong trường hợp cụ thể này, việc thay đổi cách
thức làm việc của hàm strstr có thể tăng hiệu quả hoạt động của hệ thống.
Hàm strstr được cài đặt lại như sau:
/* Hàm strstr :
dùng hàm strchr để tìm kiếm kí tự đầu tiên */
char* strstr(const char *s1, const char *s2)
Trang 11nào đó, vì chỉ dùng các hàm được tối ưu hóa cao để thực hiện công việc Đoạnchương trình này gọi hàm strchr để tìm vị trí tiếp theo của ký tự đầu tiêntrong chuỗi, sau đó gọi hàm strncmp để kiểm tra phần còn lại của chuỗi có sokhớp với phần còn lại của chuỗi mẫu Như vậy, đoạn chương trình này sẽ bỏqua một phần lớn của bức thông điệp để tìm ký tự đầu tiên của chuỗi mẫu, rồiquét nhanh để kiểm tra phần còn lại Thế nhưng, tại sao tốc độ thực hiệnchương trình khi sử dụng hàm strstr được cải tiến vẫn còn quá chậm?
Hiện tượng trên có thể được lý giải bằng các lý do chính sau đây Trướctiên, hàm strncmp lấy chiều dài chuỗi mẫu bằng strlen làm tham số đầuvào Thế nhưng vì chuỗi mẫu có chiều dài cố định nên việc tính lại độ dài chuỗimẫu khi xử lý mỗi thông điệp là không cần thiết
Thứ hai, hàm strncmp có chứa một vòng lặp phức tạp Thực tế, không
những hệ thống phải so sánh từng byte của hai chuỗi mà phải tìm byte cuối ‘\
0’ trên cả hai chuỗi trong khi vẫn đếm lùi tham số chiều dài Vì chiều dài củatất cả các chuỗi đều đã biết trước (dù không cần dùng tới hàm strncmp) nên sựphức tạp này là không cần thiết Ta thấy rằng phép đếm là cần thiết, nhưng việc
kiểm tra byte ‘\0’ sẽ lãng phí nhiều thời gian.
Thứ ba, hàm strnchr cũng phức tạp, bởi vì đồng thời vừa tìm ký tự và
kiểm tra byte ‘\0’ kết thúc bức thông điệp Trong một lần gọi hàm isspam,
bức thông điệp là cố định, vì vậy thời gian dùng để kiểm tra byte ‘\0’ là lãngphí vì ta đã biết chính xác bức thông điệp kết thúc ở đâu
Cuối cùng, dù cho hàm strncmp, strchr, và strlen đều rất hiệu quả khilàm việc độc lập, tổng thời gian cần để gọi các hàm này cũng xấp xỉ với thờigian tính toán của chúng Sẽ hiệu quả hơn nếu làm mọi việc bằng chỉ một hàmstrstr được viết lại thật cẩn thận và tránh gọi tất cả những hàm khác
Những vấn đề thuộc dạng này chính là một nguồn gốc chung làm chậm tốc
độ thực hiện chương trình – một thủ tục hoặc một phương thức giao tiếp làmviệc tốt trong một tình huống điển hình nhưng lại thực hiện kém hiệu quả trong
Trang 12tình huống bất thường xảy ra trở thành vấn đề trung tâm của chương trình Hàmstrstr có sẵn thực hiện tốt khi chuỗi mẫu và chuỗi cần tìm ngắn và thay đổi ởmỗi lần hàm được gọi Nhưng khi chuỗi cần tìm dài và cố định thì thời gian thờigian xử lý lại bị lãng phí quá nhiều.
Từ những nhận xét trên đây, ta quyết định viết lại hàm strstr để xử lý trênchuỗi mẫu cùng với chuỗi thông điệp nhằm tìm ra chỗ so khớp mà không cầngọi các thủ tục con Khả năng thực hiện khi thực hiện giải pháp này hoàn toàn
có thể dự đoán được: chương trình có thể hơi chậm trong một số trường hợp cá
biệt, nhưng lại rất nhanh trong việc lọc các “spam” và quan trọng nhất là không
bao giờ chạy quá lâu Để kiểm tra lại tính đúng đắn của bản cài đặt mới cũngnhư tốc độ thực hiện của chương trình mới, ta xây dựng một bộ kiểm chứng tốc
độ thực hiện Bộ thử này không chỉ gồm các ví dụ đơn giản như tìm một từtrong câu, mà còn gồm cả một số trường hợp đặc biệt, ví dụ như tìm chuỗi mẫuchỉ có một ký tự ‘x’ trong chuỗi có một ngàn ký tự ‘e’, và chuỗi mẫu có mộtngàn ký tự ‘x’ trong một chuỗi chỉ có một ký tự ‘e’ Những trường hợp đặc biệtnhư vậy là phần chính trong việc đánh giá khả năng thực hiện
Thư viện được cập nhật bằng hàm strstr mới và bộ lọc “spam” chạy
nhanh hơn 30%, đây là một kết quả cải tiến đáng kể cho việc viết lại
Tuy nhiên, tốc độ thực hiện vẫn còn quá chậm Khi giải quyết các khó khăn,điều quan trọng là xác định đúng vấn đề cần giải quyết Cho đến nay, ta đangđặt vấn đề tìm cách nhanh nhất để truy soát một chuỗi ký tự mẫu trong mộtchuỗi ký tự Tuy nhiên, thật ra thì vấn đề là tìm kiếm một tập hợp lớn, cố địnhcác chuỗi ký tự mẫu trong một chuỗi ký tự dài thay đổi Trong trường hợp đó,hàm strstr chưa thật sự là giải pháp đúng
Cách hiệu quả nhất để làm cho một chương trình chạy nhanh hơn là dùngmột giải thuật tốt hơn Bây giờ, khi đã nhìn rõ hơn vấn đề cần giải quyết, ta cầnsuy nghĩ xem thuật toán nào sẽ làm việc tốt nhất
Xét vòng lặp cơ bản:
Trang 13for (i=0; i < npat; i++)
if (strstr(meg, pat[i]) != NULL)
return 1;
Vòng lặp này duyệt qua bức thông điệp npat lần độc lập với nhau; giả sử đoạn
lệnh không tìm ra bất kỳ sự so khớp nào, đoạn lệnh này đã phân tích từng byte
của bức thông điệp npat lần trong strlen(mesg)*npat phép so sánh
Một cách tiếp cận tốt hơn là đảo ngược vòng lặp, quét qua bức thông điệpmột lần ở vòng lặp ngoài trong khi tìm tất cả các chuỗi mẫu song song ở vònglặp trong:
for (j=0; mesg[j] != '\0'; j++)
if (có chuỗi mẫu nào có phần đầu trùng với
ký tự đầu của mesg[j])
return 1;
Sự cải thiện tốc độ bắt nguồn từ cách nhìn hết sức đơn giản Để nhận ra liệu
có chuỗi mẫu nào so khớp với bức thông điệp ở vị trí j không, ta không cầnphải xét tất cả các chuỗi mẫu mà chỉ cần xét các chuỗi có cùng ký tự đầu tiênvới mesg[j] Nói cách khác, với 52 ký tự chữ hoa và chữ thường, ta chỉ cầnthực hiện strlen(mesg)*npat/52 phép so sánh Vì các chữ cái không phân
bố đều, có nhiều từ bắt đầu bằng ký tự ‘s ’ hơn là bằng ký tự ‘x’, ta không thể
kết luận rằng giải pháp này cải thiện tốc độ nhanh hơn gấp 52 lần phiên bảntrước, tuy nhiên, rõ ràng việc tối ưu hóa này đem lại kết quả rất đáng kể Đểthực hiện, ta xây dựng một bảng băm dùng ký tự đầu tiên của chuỗi mẫu làmkhóa
Dựa vào một vài tính toán trước để xây dựng một bảng những chuỗi mẫu bắtđầu với từng ký tự, hàm isspam vẫn được viết ngắn gọn như sau:
int patlen[NPAT];
/* mảng chiều dài của các chuỗi mẫu */
int starting[UCHAR_MAX+1][NSTART];
/* danh sách các chuỗi mẫu bắt bằng ký tự tương ứng */int nstarting[UCHAR_MAX+1];
/* số chuỗi mẫu bắt đầu bằng ký tự tương ứng */
Trang 14
/* Hàm isspam: kiểm tra chuỗi thông điệp mesg có chứaspam nào không */
int isspam(char *mesg)
}
return 0;
}
Với mỗi ký tự ‘c ’, mảng hai chiều starting[c][] chứa danh sách các
chuỗi mẫu bắt đầu bằng ký tự ‘c ’ Mảng nstarting[c] ghi nhận bao nhiêu
chuỗi mẫu bắt đầu bằng ký tự ‘c ’ Nếu không có các bảng đó, vòng lặp trong sẽ
chạy từ 0 đến npat, khoảng 1000, thay vì chỉ chạy từ 0 đến khoảng 20 Cònphần tử mảng patlen[k] chứa kết quả tính toán trước củastrlen(pat[k]).
Hình dưới đây phác họa các cấu trúc dữ liệu trên dùng một tập hợp ba chuỗi
mẫu bắt đầu bằng ký tự ‘b ’:
Trang 15để lưu trữ thông tin của các chuỗi ký tự mẫu
Dưới đây là đoạn chương trình để xây dựng các bảng chứa thông tin cácchuỗi mẫu:
Phụ thuộc vào dữ liệu nhập, bộ lọc “spam” bây giờ cải tiến chạy nhanh hơn
từ 5 đến 10 lần khi dùng hàm strstr, và nhanh hơn từ 7 đến 15 lần khi dùngphương pháp đầu tiên Tốc độ thực hiện không thể đạt được mức nhanh hơn 52lần so với phiên bản đầu tiên, một phần do sự phân bố không đều của bảng chữ
Trang 16cái, một phần do vòng lặp trong chương trình mới phức tạp hơn, một phần khác
do vẫn còn phải thực hiện quá nhiều phép so chuỗi không đạt, nhưng bộ lọc
“spam” không gây ra hiện tượng nút cổ chai gây tắt nghẽn việc phân phối thư
nữa Vấn đề tốc độ thực hiện chương trình đã được giải quyết
Phần còn lại của chương này sẽ đi sâu vào các kỹ thuật được dùng để pháthiện các vấn đề về tốc độ thực hiện chương trình, xác định các đoạn mã chậm
và tăng tốc cho chúng Trước khi tiếp tục, ta cần xem lại ví dụ về bộ lọc
“spam” nhằm rút ra những bài học cần thiết Nếu bộ lọc “spam” không phải là
một nút cổ chai thì tất cả các cố gắng mà ta vừa thực hiện đều trở nên không
còn ý nghĩa Một khi đã xác định được vấn đề, ta sử dụng kỹ thuật profiling và
những kỹ thuật khác để khảo sát và phân tích việc thực hiện của chương trìnhnhằm tìm ra mấu chốt vấn đề thật sự ở đâu Sau đó, ta phải chắc chắn rằng ta đãgiải quyết đúng vấn đề, thử nghiệm toàn bộ chương trình chứ không chỉ tậptrung vào hàm strstr, là đầu mối nghi ngờ ban đầu, thoạt đầu tưởng là đã rõnhưng thực ra lại là không đúng Cuối cùng, ta giải quyết đúng vấn đề bằng mộtgiải thuật tốt hơn, và kiểm nghiệm lại xem chương trình đã chạy nhanh hơnchưa Một khi chương trình đã chạy đủ nhanh, ta sẽ dừng lại Việc suy nghĩ tìmcách cải tiến tiếp tục là không cần thiết?
Bài tập 1.1 Hãy cài đặt một phiên bản isspam dùng hai ký tự làm chỉ mục.Hãy cho biết mức độ cải tiến của hệ thống trong trường hợp này? Đây là những
trường hợp đơn giản của kiểu cấu trúc dữ liệu được gọi là trie Đặc tính chung
của đa số những cấu trúc dữ liệu này là chấp nhận tốn không gian lưu trữ đểthời gian thực hiện chương trình nhanh hơn
1.2 Kỹ thuật đo thời gian thực hiện và lưu lại quá trình hoạt động (Profiling)
1.2.1 Đo thời gian tự động
Đa số hệ thống đều có lệnh để đo thời gian chạy một chương trình TrênUnix, lệnh này là time:
Trang 17có được (chẳng hạn như phiên bản nào của chương trình chạy nhanh hơn20%?) Nhiều kỹ thuật đã nghiên cứu trong Chương 6 có thể được đưa vào để
đo đạc và cải tiến tốc độ thực hiện của chương trình Bạn nên cho chương trìnhthực thi thật sự trên máy tính và đo đạt các số liệu với các bộ dữ liệu kiểmchứng, và điều quan trọng nhất là dùng kỹ thuật kiểm tra hồi quy để bảo đảmcác sửa đổi không làm mất đi tính đúng đắn của chương trình
Nếu hệ thống của bạn không có lệnh time, hay nếu bạn đang đo thời giancủa một hàm cụ thể nào đó, bạn có thể dễ dàng xây dựng một cấu trúc đo thờigian tương tự Trong C và C++ đều có cung cấp một hàm chuẩn, hàm clock,dùng để tính thời gian CPU mà chương trình đã dùng cho đến thời điểm hiệntại Hàm này có thể được gọi trước và sau một hàm để đo thời gian sử dụngCPU:
Trang 18elapsed = clock() - before;
printf("function used %.3f seconds\n",
elapsed/CLOCKS_PER_SEC);
Hằng số CLOCKS_PER_SEC xác định số lần dao động của bộ đếm (timer)
hệ thống trong 1 giây Trong trường hợp hàm thực hiện trong một phần rất nhỏcủa một giây, ta có thể cho thực hiện nhiều lần hàm này bằng một vòng lặp, tuynhiên, khi đó, ta cần lưu ý đến chi phí của bản thân vòng lặp nếu thời gian nàyđáng kể so với tổng thời gian thực hiện của hàm cần đo:
before = clock();
for (i=0; i<1000; i++)
short_running_function();
elapsed = (clock() - before)/(double)i;
Trong Java, ta có thể sử dụng các hàm trong lớp Date để xấp xỉ thời gianCPU:
Date before = new Date();
long_running_function();
Date after = new Date();
long elapsed = after.getTime() - before.getTime();Hàm getTime trả về giá trị tính bằng miligiây
1.2.2 Kỹ thuật profiling – lưu lại quá trình thực hiện
Bên cạnh phương pháp đo thời gian đã trình bày ở trên, ta còn có một công
cụ quan trọng hỗ trợ phân tích việc thực thi chương trình: đó chính là kỹ thuậtprofiling - một hệ thống giúp lưu lại thông tin quá trình thực hiện Kỹ thuật nàygiúp ghi nhận lại việc gọi thực hiện và thời gian thực hiện từng thao tác trongchương trình Một số hệ thống ghi nhận chi tiết từng hàm, số lần mà hàm nàyđược gọi thực hiện và tỷ lệ thời gian đã sử dụng để thực hiện từng hàm so với
Trang 19toàn bộ thời gian thực hiện đoạn chương trình Một số hệ thống ghi nhận lại sốlần từng phát biểu trong chương trình được thực hiện Những phát biểu đượcthực hiện nhiều lần sẽ chiếm phần lớn thời gian thi hành, trong khi đó, nhữngphát biểu không bao giờ được thực hiện chính là những đoạn mã thừa hoặcchưa được kiểm tra đầy đủ.
Kỹ thuật ghi nhận quá trình thực hiện là công cụ hiệu quả để tìm các đoạn
mã chính trọng tâm của chương trình, tức là những hàm hoặc những đoạn mãchiếm phần lớn thời gian tính toán Tuy nhiên, việc sử dụng và phân tích các sốliệu kết quả của kỹ thuật này cần được tiến hành một cách thận trọng Do tínhchất phức tạp của trình biên dịch cũng như trong các hiệu ứng của bộ nhớ đệm
và bộ nhớ chính, cùng với việc ghi nhận quá trình thực hiện của chương trình sẽlàm ảnh hưởng đến tốc độ thực hiện của chương trình này nên các thống kê của
kỹ thuật này chỉ là kết quả xấp xỉ
Kỹ thuật ghi nhận quá trình thực hiện thường được kích hoạt bằng một cờcủa trình biên dịch hoặc chức năng đặc biệt Chương trình được cho thực hiện,
và sau đó một công cụ phân tích sẽ biểu diễn các kết quả Trên Unix, ta thường
sử dụng cờ -p và công cụ prof như sau:
% cc –p spamtest.c –o spamtest
% spamtest
% prof spamtest
Bảng sau đây thể hiện kết quả ghi nhận quá trình thực hiện chương trình bộ
lọc spam Chương trình dùng một bức thông điệp cố định và một tập hợp cố
định gồm 217 cụm từ mẫu, tập hợp này so khớp với bức thông điệp 10000 lần.Chương trình chạy trên máy 250 MHz MIPS R1000 dùng bản gốc của hàmstrstr trong đó gọi các hàm chuẩn khác Cần chú ý sự xuất hiện các giá trịkích cỡ của dữ liệu đầu vào (217 cụm từ) và số lần chạy (10000) trong cột sốlần gọi thực hiện
12234768552: Tổng số lệnh đượcthực hiện
13961810001: Tổng số chu kỳ tính toán
Trang 2055.847: Tổng thời gian tính toán (giây)
1.141: Số chu kỳ trung bình/lệnh
Giây % cum% Số chu kỳ Số lệnh Số lần gọi Hàm45.260 81.0 81.0 11314990000 9440110000 48350000 strchr6.081 10.9 91.9 1520280000 1566460000 46180000 strncmp2.592 4.6 96.6 648080000 854500000 2170000 strstr1.825 3.3 99.8 456225559 344882213 2170435 strlen0.088 0.2 100.0 21950000 28510000 10000 isspam0.000 0.0 100.0 100025 100028 1 main
0.000 0.0 100.0 53677 80268 219 _memcopy0.000 0.0 100.0 48888 46403 217 strcpy0.000 0.0 100.0 17989 19894 219 fgets0.000 0.0 100.0 16798 17547 230 malloc0.000 0.0 100.0 10305 10900 204 realfree0.000 0.0 100.0 6293 7161 217 estrdup0.000 0.0 100.0 6032 8575 231
cleanfree
0.000 0.0 100.0 5932 5729 1 readpat0.000 0.0 100.0 5899 6339 219 getline0.000 0.0 100.0 5500 5720 220 _malloc
Bảng 1.1 Bảng thống kê quá trình thực hiện chương trình spamtest nguyên thủy
Rõ ràng hàm strchr và hàm strncmp, cả hai do hàm strstr gọi, chiếmhầu hết thời gian thực hiện chương trình Nhận xét của tác giả Knuth hoàn toànchính xác: chỉ một phần nhỏ của chương trình đã chiếm gần hết thời gian chạy
chương trình Khi một chương trình lần đầu tiên được xây dựng profile, hàm
được gọi nhiều nhất thường chiếm đến 50% hoặc hơn thời gian thực hiệnchương trình, như ở trường hợp này Điều này.giúp ta dễ dàng xác định cần tậptrung chú ý vào phần nào trong chương trình để cải tiến việc thực hiện
1.2.3 Tập trung quan tâm vào các đoạn mã trọng tâm của
chương trình
Sau khi viết lại hàm strstr, ta ghi nhận lại quá trình thực hiện của chươngtrình spamtest Lúc này, ta nhận thấy rằng 99.8% thời gian bây giờ bị chiếmbởi hàm strstr, mặc dù cả chương trình đã nhanh hơn rõ rệt Khi gặp tìnhtrạng có duy nhất một hàm gây ra hiện tượng nút cổ chai ta thường có hai con
Trang 21đường để chọn lựa: cải tiến hàm đó bằng một thuật toán tốt hơn, hoặc bỏ toàn
bộ hàm đó bằng cách viết lại cả chương trình
Trong trường hợp này, ta chọn giải pháp viết lại toàn bộ chương trình Dướiđây là một vài dòng kết quả đầu tiên của việc ghi nhận quá trình thực hiện củachương trình spamtest dùng bản cài đặt có tốc độ cao của hàm isspam Cầnchú ý rằng tổng thời gian toàn bộ đã giảm đi rất nhiều, trong đó hàm memcmp là
hàm được gọi xử lý nhiều nhất (hot spot) và hàm isspam bây giờ chiếm mộtphần đáng kể trong thời gian tính toán Phiên bản này phức tạp hơn so với phiênbản sử dụng hàm strstr nhưng ít tốn chi phí hơn nhờ không sử dụng hàmstrlen và strchr trong hàm isspam và việc thay thế hàm strncmp bằnghàm memcmp có chi phí xử lý đối với từng byte thấp hơn.
Giây % cum% Số chu kỳ Số lệnh số lần gọi Hàm3.524 56.9 56.9 880890000 1027590000 46180000 memcmp2.662 43.0 100.0 665550000 902920000 10000 isspam0.001 0.0 100.0 140304 106043 652 strlen0.000 0.0 100.0 100025 100028 1 main
Bảng 1.2 Bảng thống kê quá trình thực hiện
chương trình spamtest cải tiến
Hãy so sánh số chu kỳ đếm được và số lần gọi trong hai kết quả thống kêquá trình thực hiện chương trình Cần lưu ý rằng số lần gọi hàm strlen đãgiảm từ vài triệu lần gọi xuống còn 652 lần, số lần gọi hàm strncmp bằng với
số lần gọi hàm memcmp (có chi phí xử lý đối với từng byte thấp hơn) Cũng cần
lưu ý là hàm isspam sau khi tích hợp hàm strchr vẫn sử dụng số chu kỳ íthơn nhiều so với hàm strchr trước đó vì hàm này chỉ kiểm tra các chuỗi mẫuthích hợp ở mỗi bước
Một đoạn lệnh khi thực hiện chiếm phần lớn thời gian xử lý của toàn bộchương trình, tạm gọi là điểm nóng (hot spot), có thể được loại bỏ hay “làmnguội” đi bằng một số kỹ thuật đơn giản hơn so với cách ta đã dùng cho chương
trình bộ lọc spam Xét ứng dụng Awk, kết quả ghi nhận cho thấy một hàm được
gọi thực hiện đến hàng triệu lần trong vòng lặp sau đây:
Trang 22for (j=i; j < MAXFLD; j++)
clear(j);
Vòng lặp này, thực hiện việc xóa các trường trước khi đọc vào một dòngmới, đã chiếm hơn 50% thời gian thi hành Hằng số MAXFLD (số trường tối đacủa một dòng) bằng 200 Tuy nhiên, trong hầu hết các ứng dụng Awk, sốtrường thực sự được sử dụng và cần được xóa chỉ gồm hai đến ba trường Dovậy, rất nhiều thời gian bị lãng phí vào việc xóa các trường không bao giờ đượcxác lập Khi thay hằng số trên bằng giá trị số lượng tối đa các trường trong lầnthực hiện trước đó cho phép tăng tốc toàn bộ chương trình lên khoảng 25%.Chúng ta chỉ cần thay đổi giá trị cận trên của vòng lặp:
for (j=i; j < maxfld; j++)
clear(j);
maxfld = i
1.2.4 Biểu diễn trực quan số liệu thống kê hiệu năng thực hiện
của chương trình
Hình vẽ thường rất hữu ích trong việc thể hiện trực quan các số liệu thống
kê hiệu năng thực hiện của chương trình Các hình vẽ có khả năng biểu diễntrực quan thông tin về hiệu quả của việc thay đổi các tham số, so sánh các thuậttoán và các cấu trúc dữ liệu, và đôi khi còn chỉ ra được những trường hợpchương trình thực hiện theo cách không mong muốn
Đồ thị dưới đây thể hiệu ảnh hưởng của kích thước mảng các bảng băm lên
thời gian thi hành chương trình của giải thuật Markov (viết bằng C) với dữ liệu
đầu vào gồm 42685 từ và 22482 tiếp đầu ngữ Ta hãy làm hai thử nghiệm Mộtthử nghiệm gồm một số các lần chạy chương trình dùng các mảng có kích thước
là lũy thừa của 2 từ 2 đến 16384; thử nghiệm kia dùng các mảng có kích thước
là số nguyên tố lớn nhất không vượt quá lũy thừa của 2 gần nhất Ta xem liệukích thước của mảng là số nguyên tố có tạo ra sự khác biệt nào trong tốc độthực hiện chương trình hay không
Trang 23Hình 1.1 Đồ thị thể hiện sự biến thiên tốc độ xử lý của chương trình với kích thước của bảng băm trong trường hợp kích thước là lũy thừa của 2
và trường hợp kích thước là số nguyênt tố
Đồ thị trên cho thấy thời gian thực hiện của chương trình gần như khôngthay đổi đối với kích thước bảng băm lớn hơn hay bằng 1000, đồng thời kíchthước bảng băm là lũy thừa của 2 hay là số nguyên tố không phải là yếu tố ảnhhưởng đến tốc độ thực hiện chương trình
Bài tập 1.2 Cho dù hệ thống bạn đang dùng có lệnh time hay không, hãy
sử dụng hàm clock hay hàm getTime để viết một tiện ích đo thời gian chomục đích dùng riêng của bạn Hãy so sánh độ chính xác của kết quả đo đượcbằng công cụ do bạn xây dựng với kết quả khi sử dụng đồng hồ bên ngoài(chẳng hạn như đồng hồ treo tường) Những hoạt động khác trong máy tính cóảnh hưởng như thế nào đến việc đo thời gian?
Bài tập 1.3 Ở bảng kết quả thống kê quá trình hoạt động đầu tiên, hàm
strchr được gọi thực hiện 48350000 lần trong khi hàm strncmp chỉ gọi thực
hiện 46180000 lần Hãy giải thích sự khác biệt này
5020105
12
Kích thước bảng băm
1 10 100 1000 10000
0.50.2
Thời gian
(giây)
Số nguyên tố Lũy thừa của 2
Trang 241.3 Các chiến lược tối ưu hóa thời gian thực hiện
Trước khi bắt đầu sửa đổi một chương trình để tăng tốc độ xử lý nhanh hơn,hãy chắc rằng chương trình hiện tại chạy thật sự quá chậm, và hãy dùng cáccông cụ đo thời gian và công cụ ghi nhận quá trình thực hiện để xác định đượcphần lớn thời gian xử lý của chương trình làm những công việc gì Một khi đãbiết xác định được vấn đề, ta có nhiều áp dụng nhiều chiến lược để thực hiệnmục đích tối ưu hóa thời gian thực hiện Dưới đây là một số chiến lược nhằmtối ưu hóa thời gian thực hiện theo thứ tự hiệu quả giảm dần
1.3.1 Sử dụng thuật toán hoặc cấu trúc dữ liệu tốt hơn
Yếu tố quan trọng nhất làm cho chương trình chạy nhanh hơn là lựa chọnthuật toán và cấu trúc dữ liệu hiệu quả Trên thực tế, có sự khác biệt rất lớn giữamột thuật toán hiệu quả và một thuật toán không hiệu quả Như đã trình bày ở
phần 1.1-Hiện tượng nút cổ chai, đối với chương trình bộ lọc spam, việc thay
đổi cấu trúc dữ liệu giúp cải thiện tốc độ nhanh hơn 10 lần, và hệ số tốc độ nàycòn có thể được cải thiện lớn hơn nếu như thuật toán có độ phức tạp giảm từ
O(n2) còn O(n logn) Điều này đã được phân tích chi tiết ở Chương 2 - Thuật
toán và Cấu trúc Dữ liệu
Vấn đề xác định độ phức tạp của thuật toán cũng như chương trình là vấn đề
khá phức tạp Đoạn lệnh duyệt 1 chuỗi ký tự s dưới đây dường như có độ phức tạp tuyến tính nhưng thật ra độ phức tạp của thuật toán này lại là bậc 2 (O(n2))
Nếu chuỗi s có n ký tự, mỗi lần gọi hàm strlen sẽ thực hiện việc duyệt qua n
ký tự của chuỗi, trong khi vòng lặp được thực hiện n lần.
? for (i = 0; i < strlen(s); i++)
? if (s[i] == c)
1.3.2 Sử dụng các chức năng tối ưu hóa của trình biên dịch
Chúng ta có thể cải thiện đáng kể tốc độ xử lý của chương trình một cáchđơn giản mà không cần sửa đổi bất kỳ một dòng mã nguồn nào bằng cách sử
Trang 25dụng hiệu quả các chức năng tối ưu hóa có sẵn trong trình biên dịch Các trìnhbiên dịch hiện đại ngày nay có khả năng tối ưu hóa hiệu quả các đoạn mã nguồnchương trình.
Việc sử dụng các chiến lược tối ưu hóa của trình biên dịch có khuynh hướnggây xáo trộn việc bắt lỗi (debug) mã nguồn Do đó, ở chế độ mặc định, các trìnhbiên dịch C và C++ thường không sử dụng nhiều các chức năng tối ưu hóa chochương trình Các tùy chọn trong trình biên dịch cho phép lập trình viên quyếtđịnh sử dụng hay không các chức năng tối ưu hóa của trình biên dịch Lập trìnhviên thường cần phải kích thước tường minh tùy chọn tối ưu hóa mã nguồn củatrình biên dịch sau khi đã kiểm lỗi xong chương trình
Việc tối ưu hóa thông qua chức năng của trình biên dịch thường cải thiệnthời gian chạy chương trình từ vài phần trăm cho đến hai trăm phần trăm Tuynhiên, việc tối ưu hóa này đôi khi lại có thể làm giảm tốc độ chương trình, dovậy cần đo lường sự cải thiện trước khi đóng gói sản phẩm Đối với chương
trình bộ lọc spam, với cùng dữ liệu thử nghiệm, chương trình sử dụng phiên bản
thuật toán so trùng khớp cuối cùng có thời gian thi hành ban đầu là 8.1 giâygiảm xuống còn 5.9 giây khi sử dụng chức năng tối ưu hóa của trình biên dịch(mức độ cải thiện là 25%) Ngược lại, ở phiên bản dùng hàm strstr đã sửađổi, việc tối ưu hóa không đem lại một cải thiện nào bởi lẽ hàm strstr đãđược tối ưu hóa khi đưa vào thư viện: bộ tối ưu hóa chỉ áp dụng vào mã nguồnđang được biên dịch trong chương trình chứ không phải vào các thư viện của hệthống Tuy nhiên, một số trình biên dịch có các bộ tối ưu hóa toàn cục có khảnăng phân tích toàn bộ chương trình tìm những chỗ còn có thể cải thiện được.Nếu một trình biên dịch như thế có trong hệ thống của bạn, hãy thử sử dụng;điều này có thể giúp giảm được một vài chu kỳ xử lý
Một điều cần phải hiểu rõ là càng cho phép trình biên dịch tối ưu hóa nhiềubao nhiêu thì khả năng đưa vào lỗi trong chương trình càng nhiều bấy nhiêu.Sau khi sử dụng chức năng tối ưu hóa của trình biên dịch, bạn cần sử dụng các
Trang 26bộ dữ liệu thử nghiệm để kiểm soát việc tối ưu hóa này không làm mất đi tínhđúng đắn của chương trình.
1.3.3 Tinh chỉnh mã nguồn
Chọn lựa thuật toán thích hợp và hiệu quả là điều rất quan trọng khi kíchthước dữ liệu lớn Hơn thế nữa, việc cải thiện do thuật toán có tác dụng trên mọimáy, mọi trình biên dịch, mọi ngôn ngữ lập trình Nhưng trong trường hợp đã
có thuật toán đúng mà tốc độ vẫn còn là vấn đề cần giải quyết, bạn nên thử tinhchỉnh mã nguồn: điều chỉnh chi tiết các vòng lặp và phát biểu sao cho tốc độthực hiện nhanh hơn
Phiên bản isspam đã xét ở cuối phần 1.1-Hiện tượng nút cổ chai chưa đượctinh chỉnh Chúng ta sẽ tiến hành “trau chuốt” đoạn mã nguồn chương trình.Dưới đây là đoạn mã nguồn trước khi cải tiến:
}
Phiên bản ban đầu này với bộ dữ liệu thử nghiệm chọn trước thực hiện trong6.6 giây (khi đã sử dụng bộ tối ưu hóa của trình biên dịch) Vòng lặp trong cómột chỉ số mảng (nstarting[c]) trong điều kiện lặp, mà giá trị của chỉ số nàykhông đổi ở mỗi lần lặp của vòng lặp ngoài Ta có thể tránh tính lại giá trị đóbằng cách lưu vào một biến cục bộ:
Trang 27cụ thể.
Chúng ta còn có thể thực hiện thêm một thay đổi khác đối với bộ lọc spam.
Vòng lặp trong so sánh toàn bộ chuỗi mẫu với chuỗi cần xét, nhưng theo thuậttoán thì chắc chắn ký tự đầu tiên của 2 chuỗi đã giống nhau Do vậy, ta có thểtinh chỉnh mã nguồn để bắt đầu hàm memcmp từ byte tiếp theo Thử nghiệmcho thấy sự tinh chỉnh này giúp tăng tốc độ 3%, tuy không nhiều nhưng chỉ phảiđiều chỉnh 3 dòng của chương trình
1.3.4 Không tối ưu hóa những gì không gây vấn đề
Đôi khi sự tinh chỉnh mã nguồn không đem lại kết quả gì bởi vì được ápdụng đối với những chỗ không phù hợp Cần chắc chắn rằng đoạn mã đangđược tối ưu hóa thật sự là chỗ chiếm nhiều thời gian chạy trong chương trình.Câu chuyện sau đây có thể là ngụy tạo, nhưng cũng nên kể ra làm ví dụ Mộtcông ty nay không còn tồn tại nữa, sử dụng một công cụ theo dõi tốc độ thựchiện của phần cứng ở một chiếc máy đời đầu và thấy rằng máy dùng đến 50phần trăm thời gian thực hiện cùng một dãy vài chỉ thị Các kỹ sư đã tạo mộtchỉ thị để đóng gói lại thành một hàm, chạy lại, và thấy rằng họ không thu đượckết quả gì; họ đã tối ưu hóa vòng lặp nghỉ của hệ điều hành
Trang 28Một vấn đề khác là chúng ta cần nỗ lực đến mức nào trong việc tăng tốc độchương trình? Tiêu chí chủ yếu là liệu các thay đổi có đem lại lợi ích xác đángnào hay không? Một quy tắc đơn giản là tổng thời gian dùng cho việc cải tiếntăng tốc độ cho chương trình không được vượt quá thời gian mà sự tăng tốc cóthể đem lại được trong suốt thời gian tồn tại của chương trình Theo quy tắcnày, việc cải tiến thuật toán đối với hàm isspam là đáng giá: mất một ngày làmviệc nhưng lại làm lợi được nhiều giờ đồng hồ mỗi ngày Việc loại bỏ chỉ sốmảng ra khỏi vòng lặp bên trong tuy không phải là vấn đề lớn nhưng vẫn có giátrị vì chương trình cung cấp dịch vụ cho một cộng đồng lớn Tối ưu hóa các
dịch vụ công cộng (như bộ lọc spam) hay một thư viện hầu như lúc nào cũng có
giá trị, còn việc tăng tốc độ xử lý cho các chương trình kiểm chứng hầu nhưkhông có giá trị Đối với chương trình hoạt động liên tục nhiều năm, bạn nên tối
ưu hóa tất cả những gì có thể được Thậm chí, sau nhiều tháng sử dụng chươngtrình, bạn có thể tiếp tục việc cải tiến tối ưu hóa chương trình nếu giải pháp mới
có thể giúp tăng 10% tốc độ chương trình
Các chương trình có tính cạnh tranh như trò chơi, trình biên dịch, xử lý vănbản, bảng tính, hệ thống cơ sở dữ liệu đều thuộc nhóm này Tốc độ xử lý ảnhhưởng rất lớn đến sự thành công của các sản phẩm thương mại, ít nhất thôngqua trong các kết quả chấm điểm (benchmark)
Việc đo thời gian của chương trình khi thực hiện các thay đổi là rất quantrọng để chắc chắn rằng chương trình vẫn đang được cải thiện Đôi khi hai cảitiến khác nhau có thể có ích một cách riêng rẽ nhưng khi kết hợp với nhau sẽloại trừ lẫn nhau! Cũng có trường hợp cơ chế đo thời gian hoạt động thấtthường đến mức khó rút ra được kết luận chính xác về tác dụng của các thayđổi Ngay cả trên một hệ thống chỉ có một người dùng, thời gian cũng có thểdao động ngoài dự tính Nếu sự biến thiên của đồng hồ thời gian nội bộ (hay ítnhất là những gì được báo lại) có thể lên đến 10%, do đó, những thay đổi đemlại sự cải thiện 10% rất khó phân biệt với mức độ nhiễu này
Trang 291.4 Tinh chỉnh mã nguồn
Có nhiều kỹ thuật làm giảm thời gian thực hiện khi đã xác định được đoạn
mã nguồn chiếm phần lớn thời gian của chương trình Dưới đây là một số giảipháp đề nghị Tuy nhiên, các giải pháp này nên được áp dụng một cách thậntrọng và cần có các thử nghiệm để bảo đảm chương trình vẫn hoạt động đúng.Cần nhớ rằng, một số trình biên dịch tốt có thể làm giúp bạn một số phần, vàthực tế là bạn có thể vô tình ngăn cản điều đó bằng cách làm phức tạp chươngtrình Bất cứ những gì bạn thử, hãy đo lường hiệu quả để bảo đảm rằng điều đó
có ích
1.4.1 Tập hợp những biểu thức chung
Nếu một biểu thức tính toán tốn nhiều thời gian xuất hiện nhiều lần, hãythực hiện biểu thức này một lần duy nhất rồi lưu kết quả lại Ví dụ như ởChương 1 – Phong cách lập trình (Style), ta đã gặp một macro dùng để tínhkhoảng cách bằng cách gọi hàm sqrt hai lần trong một dòng lệnh với cùng cácgiá trị, phép tính là như sau:
? sqrt(dx*dx+dy*dy) + ((sqrt(dx*dx+dy*dy) > 0)? )Giải pháp đề nghị: Hãy tính biểu thức căn chỉ một lần và dùng giá trị đó ởhai nơi
Nếu một phép tính thực hiện bên trong vòng lặp nhưng không phụ thuộc vàonhững thay đổi bên trong vòng lặp, hãy đưa ra ngoài, như thay đoạn chươngtrình sau:
for (i = 0; i < nstarting[c]; i++)
Trang 30ta thường cần hai phép lấy căn bậc hai Tuy nhiên ta có thể chỉ cần so sánh bìnhphương của khoảng cách:
if (dx1*dx1+dy1*dy1 < dx2*dx2+dy2*dy2)
cũng cho cùng kết quả như khi so sánh các căn bậc hai
Một ví dụ tương tự đối với trường hợp so khớp các mẫu văn bản với chuỗi
ký tự cho trước trong bộ lọc spam Nếu chuỗi mẫu bắt đầu với một ký tự trong
bảng chữ cái, ta thực hiện phép tìm kiếm (đơn giản) ký tự này từ đầu đến cuốiđoạn văn bản đầu vào; nếu không tìm thấy thì cơ chế tìm kiếm tốn nhiều thờigian hơn sẽ hề không được gọi thực hiện