Tối ưu mã nguồn C/C++ Tại sao phải tối ưu mã lệnh? Sự ra đời của các trình biên dịch hiện đại đã giúp lập trình viên cải thiện đáng kể thời gian và công sức phát triển phần mềm. Một vấn đề đáng quan tâm là xu hướng phát triển phần mềm theo hướng trực quan nhanh và tiện dụng dần làm mặt bằng kĩ năng viết mã lệnh của các lập trình viên giảm rõ rệt vì họ trông cậy hoàn toàn vào sự hỗ trợ của trình biên dịch. Khi phát triển một hệ thống phần mềm có...
Tối ưu mã nguồn C/C++ Tại phải tối ưu mã lệnh? Sự đời trình biên dịch đại giúp lập trình viên cải thiện đáng kể thời gian công sức phát triển phần mềm Một vấn đề đáng quan tâm xu hướng phát triển phần mềm theo hướng trực quan nhanh tiện dụng dần làm mặt kĩ viết mã lệnh lập trình viên giảm rõ rệt họ trơng cậy hồn tồn vào hỗ trợ trình biên dịch Khi phát triển hệ thống phần mềm có tần suất xử lý cao, ví dụ sản phẩm có chức điều phối hoạt động dây chuyền sản xuất nhà máy, bên cạnh hỗ trợ trình biên dịch mạnh cịn cần đến kĩ tối ưu mã lệnh lập trình viên Kĩ tốt biến cơng việc lập trình khơ khan, với đoạn code tưởng chừng lạnh lùng trở nên sinh động Một đoạn mã lệnh tốt tận dụng tối đa ưu điểm ngôn ngữ khả xử lý hệ thống, từ giúp nâng cao đáng kể hiệu suất hoạt động hệ thống Để chương trình hoạt động tối ưu, điều tận dụng hỗ trợ sẵn có trình biên dịch thông qua thị (directive) giúp tối ưu mã lệnh, tốc độ kích thước chương trình Hầu hết trình biên dịch phổ biến hỗ trợ tốt việc tối ưu mã biên dịch Tuy nhiên, để đạt hiệu tốt nhất, lập trình viên cần tập cho thói quen tối ưu mã lệnh từ bắt tay viết chương trình đầu tay Bài viết trình bày số gợi ý kinh nghiệm thực tế tối ưu lập trình ngơn ngữ C/C++ Tinh giản biểu thức toán học Các biểu thức tốn học phức tạp biên dịch sinh nhiều mã dư thừa làm tăng kích thước chậm tốc độ thực chương trình Do viết biểu thức phức tạp lập trình viên cần nhớ số đặc điểm sau để giúp tinh giản biểu thức: - CPU xử lý phép tính cộng trừ nhanh phép tính chia nhân Ví dụ: + Biểu thức Total = (A*B + A*C + A*D) cần phép cộng phép nhân Ta nhóm phép cộng viết thành Total = A*(B+C+D), tốc độ tính nhanh giảm phép tính nhân + Biểu thức Total = (B/A + C/A) cần phép chia viết thành Total = (B+C)/A, giúp giảm phép chia - CPU xử lý tính tốn với số nguyên (integer) chậm với số thực (float, double), tốc độ xử lý float nhanh double - Trong số trường hợp nhân chia số nguyên, sử dụng toán tử dời bit (bit shifting) nhanh tốn tử nhân chia Ví dụ: Biểu thức (A *= 128) tận dụng tốn tử dời bit sang trái thành (A MyFunc() ) > max) { // } Ta chuyển hàm MyFunc biểu thức điều kiệu sau: int temp_value = mydata->MyFunc(); if (temp_value < min) { // } else if (temp_value > max) { // } Đối với biểu thức điều kiện dạng switch case: giá trị cho case liên tục nhau, trình biên dịch tạo bảng ánh xạ (còn gọi jump table) giúp việc truy xuất đến điều kiện nhanh giảm kích thước mã lệnh Tuy nhiên giá trị khơng liên tục, trình biên dịch tạo chuỗi phép tốn so sánh, từ gây chậm việc xử lý: Ví dụ sau cho kết truy xuất tối ưu sử dụng switch case: switch (my_value) { case A: break; case B: break; case C: break; case D: default: } Trong trường hợp giá trị dùng cho case khơng liên tục, ta viết thành biểu thức if elseif else sau: switch (my_value) { case A: break; case F: break; case T: } Có thể viết thành: if (my_value == A) { // xử lý cho trường hợp A } else if (my_value == F) { // xử lý cho trường hợp F } else { // trường hợp khác } Ø Tối ưu vòng lặp Vòng lặp thành phần phản ánh khả tính tốn khơng mệt mỏi máy tính Tuy nhiên, việc sử dụng máy móc vịng lặp ngun nhân làm giảm tốc độ thực chương trình Một số thủ thuật sau giúp lập trình viên tăng tốc vịng lặp mình: - Đối với vịng lặp có số lần lặp nhỏ, ta viết lại biểu thức tính tốn mà khơng cần dùng vòng lặp Nhờ tiết kiệm khoảng thời gian quản lý tăng biến đếm vịng lặp Ví dụ cho vòng lặp sau: for( int i = 0; i < 4; i++ ) { array[i] =MyFunc(i); } viết lại thành: array[0] = MyFunc(0); array[1] = MyFunc(1); array[2] = MyFunc(2); array[3] = MyFunc(3); - Đối với vịng lặp phức tạp có số lần lặp lớn, cần hạn chế việc cấp phát biến nội phép tính lặp lặp lại bên vịng lặp mà khơng liên quan đến biến đếm lặp Ví dụ cho vòng lặp sau: int students_number = 10000; for( int i = 0; i < students_number; i++ ) { //hàm MyFunc nhiều thời gian thực double sample_value = MyFunc(students_number); CalcStudentFunc(i, sample_value); } Trong ví dụ trên, biến sample_value tính vịng lặp cách khơng cần thiết hàm MyFunc tốn nhiều thời gian, ta dời đoạn mã tính tốn ngồi vịng lặp sau: int students_number = 10000; double sample_value = MyFunc(students_number); for( int i = 0; i < students_number; i++ ) { CalcStudentFunc(i, sample_value); } - Đối với vòng lặp từ đến n phần tử sau: for( int i = 0; i < max_number; i++ ) Nên thực việc lặp từ giá trị max_number trở sau: for( int i = max_number - 1; i >=0 ; i ) Vì biên dịch thành mã máy, phép so sánh với (zero) thực nhanh với số nguyên khác Do phép so sánh vòng lặp ( i >=0 ) nhanh phép so sánh ( i < max_number) - Trong vịng lặp lớn, tốn tử prefix dạng ( i ++i) thực nhanh toán tử postfix (i i++) Nguyên nhân toán tử prefix tăng giá trị biến trước sau trả kết cho biểu thức, toán tử postfix phải lưu giá trị cũ biến vào biến tạm, tăng giá trị biến trả giá trị biến tạm Tối ưu việc sử dụng nhớ trỏ Con trỏ (pointer) gọi "niềm tự hào" C/C++, nhiên thực tế nguyên nhân làm đau đầu cho lập trình viên, hầu hết trường hợp sụp đổ hệ thống, hết nhớ, vi phạm vùng nhớ xuất phát từ việc sử dụng trỏ không hợp lý - Hạn chế pointer dereference: pointer dereference thao tác gán địa vùng nhớ liệu cho trỏ Các thao tác dereference tốn nhiều thời gian gây hậu nghiêm trọng vùng nhớ đích chưa cấp phát Ví dụ với đoạn mã sau: for( int i = 0; i < max_number; i++ ) { SchoolData->ClassData->StudentData->Array[i] = my_value; } Bằng cách di chuyển pointer dereference nhiều cấp ngồi vịng lặp, đoạn mã viết lại sau: unsigned long *Temp_Array = SchoolData->ClassData->StudentData>Array; for( int i = 0; i < max_number; i++ ) { Temp_Array[i] = my_value; } - Sử dụng tham chiếu (reference) cho đối tượng liệu phức tạp tham số hàm Việc sử dụng tham chiếu truyền nhận liệu hàm giúp tăng tốc đáng kể cấu trúc liệu phức tạp Trong lập trình hướng đối tượng, đối tượng truyền vào tham số dạng giá trị tồn nội dung đối tượng chép copy constructor thành khác truyền vào hàm Nếu truyền dạng tham chiếu loại trừ việc chép Một điểm cần lưu ý sử dụng tham chiếu giá trị đối tượng thay đổi bên hàm gọi, lập trình viên cần sử dụng thêm từ khóa const khơng muốn nội dung đối tượng bị thay đổi Ví dụ: Khi truyền đối tượng dạng giá trị vào hàm để sử dụng, copy constructor gọi void MyFunc(MyClass A) //copy constructor A gọi { int value = A.value; } Khi dùng dạng tham chiếu, đoạn mã viết thành: void MyFunc(const MyClass &A) //không gọi copy constructor { int value = A.value; } - Tránh phân mảnh vùng nhớ: Tương tự việc truy xuất liệu đĩa, hiệu truy xuất liệu vùng nhớ động giảm nhớ bị phân mảnh Một số gợi ý sau giúp giảm việc phân mảnh nhớ + Tận dụng nhớ tĩnh Ví dụ: tốc độ truy xuất vào mảng tĩnh có tốc độ nhanh truy xuất vào danh sách liên kết động + Khi cần sử dụng nhớ động, tránh cấp phát giải phóng vùng nhớ kích thước nhỏ Ví dụ ta tận dụng xin cấp phát mảng đối tượng thay đối tượng riêng lẻ + Sử dụng STL container cho đối tượng chế sử dụng nhớ riêng có khả tối ưu việc cấp phát nhớ STL cung cấp nhiều thuật toán loại liệu giúp tận dụng tối đa hiệu C++ Các bạn tìm đọc sách STL biết thêm nhiều điều thú vị - Sau cấp phát mảng đối tượng, tránh nhầm lẫn sử dụng toán tử delete[] delete: với C++, toán tử delete[] định trình biên dịch xóa chuỗi vùng nhớ, delete xóa vùng nhớ mà trỏ đến, gây tượng "rác" phân mảnh nhớ Ví dụ: int *myarray = new int[50]; delete []myarray; Với delete[], trình biên dịch phát sinh mã sau: mov ecx, dword ptr [myarray] mov dword ptr [ebp-6Ch], ecx mov edx, dword ptr [ebp-6Ch] push edx call operator delete[] (495F10h) //gọi toán tử delete[] add esp,4 Trong với đoạn lệnh: int *myarray = new int[50]; delete myarray; Trình biên dịch phát sinh mã sau: mov ecx, dword ptr [myarray] mov dword ptr [ebp-6Ch], ecx mov edx, dword ptr [ebp-6Ch] push edx call operator delete (495F10h) //gọi toán tử delete add esp,4 Sử dụng hợp lý chế bẫy lỗi try catch Việc sử dụng không hợp lý bẫy lỗi sai lầm tai hại trình biên dịch thêm mã lệnh kiểm tra đoạn mã cài đặt try catch, điều làm tăng kích thước giảm tốc độ xử lý chương trình, đồng thời gây khó khăn việc sửa chữa lỗi logic Thống kê cho thấy đoạn mã có sử dụng bẫy lỗi hiệu xuất thực giảm từ 5%-10% so với đoạn mã thông thường viết cẩn thận Để hạn chế điều này, lập trình viên nên đặt bẫy lỗi đoạn mã có nguy lỗi cao khả dự báo trước thấp Tận dụng đặc tính xử lý CPU Để đảm báo tốc độ truy xuất tối ưu, vi xử lý (CPU) 32-bit yêu cầu liệu xếp tính toán nhớ theo offset 4-byte Yêu cầu gọi memory alignment Do biên dịch đối tượng liệu có kích thước 4byte, trình biên dịch bổ sung thêm byte trống để đảm bảo liệu xếp theo quy luật Việc bổ sung làm tăng đáng kể kích thước liệu, đặc biệt cấu trúc liệu structure, class Xem ví dụ sau: class Test { bool a; int c; int d; bool b; }; Theo nguyên tắc alignment 4-byte (hai biến "c" "d" có kích thước byte), biến "a" "b" chiếm byte sau biến biến int chiếm byte, trình biên dịch bổ sung byte cho biến Kết tính kích thước lớp Test hàm sizeof(Test) 16 byte Ta xếp lại biến thành viên lớp Test sau theo chiều giảm dần kích thước: class Test { int c; int d; bool a; bool b; }; Khi đó, hai biến "a" "b" chiếm byte, trình biên dịch cần bổ sung thêm byte sau biến "b" để đảm bảo tính xếp 4-byte Kết tính kích thước sau xếp lại class Test 12 byte Tận dụng số ưu điểm khác C++ - Khi thiết kế lớp (class) hướng đối tượng, ta sử dụng phương thức "inline" để thực xử lý đơn giản cần tốc độ nhanh Theo thống kê, phương thức inline thực nhanh khoảng 5-10 lần so với phương thức cài đặt thông thường - Sử dụng ngôn ngữ cấp thấp assembly: ưu điểm ngôn ngữ C/C++ khả cho phép lập trình viên chèn mã lệnh hợp ngữ vào mã nguồn C/C++ thông qua từ khóa asm { } Lợi giúp tăng tốc đáng kể biên dịch chạy chương trình Ví dụ: int a, b, c, d, e; e = a*b + a*c; Trình biên dịch phát sinh mã hợp ngữ sau: mov eax, dword ptr [a] imul eax, dword ptr [b] mov ecx, dword ptr [a] imul ecx, dword ptr [c] add eax, ecx mov dword ptr [e], eax Tuy nhiên, ta viết rút gọn giảm phép imul (nhân), phép mov (di chuyển, chép): asm { mov eax, b; add eax, c; imul eax, a; mov e, eax; }; - Ngôn ngữ C++ cho phép sử dụng từ khóa "register" khai báo biến để lưu trữ liệu biến ghi, giúp tăng tốc độ tính tốn truy xuất liệu ghi ln nhanh truy xuất nhớ Ví dụ: for (register int i; i