Để đo thời gian thực thi của các đoạn mã ví dụ bên dưới, bạn có thể dùng DateTime.Now.Ticks lưu thời điểm bắt đầu và kết thúc. Tuy nhiên .Net cung cấp cho bạn sẵn đối tượng Stopwatch (đồng hồ bấm giờ) nằm trong không gian tên System.Diagnostics để dùng cho những công việc dạng này.
Trong mỗi phần tôi sẽ so sánh hai phương pháp (đoạn mã), phương pháp thứ hai sẽ là phương pháp tối ưu hơn cho bạn lựa chọn. Mặc dù các giải pháp thay thế có thể tốt hơn nhưng không hẳn đã là tối ưu, việc tối ưu một đoạn mã đòi hỏi sự hiểu biết và phân tích khá sâu vào nền tảng .Net, hơn nữa còn phụ vào thuật toán bạn sử dụng trong từng trường hợp.
1. So sánh chuỗi:
- (1) int String.Compare(string strA, string strB, bool ignoreCase)
- (2) bool string.Equals(string value, StringComparison comparisonType)
string s1=”aaa”;
string s2=”AAA”;
Đoạn mã 1:
for (int i = 0; i < 100000; i++){bool b = String.Compare(s1, s2,true)==0;}
Đoạn mã 2:
Đoạn mã thứ nhất chạy chậm hơn đoạn thứ hai hơn 3 lần. Tuy nhiên nếu bạn sử dụng tham số StringComparison.CurrentCultureIgnoreCase cho phương thức Equals thì tốc độ giữa hai đoạn mã là xấp xỉ. Một số người dùng cách chuyển cả hai chuỗi về dạng chữ hoa hoặc chữ thường rồi so sánh sẽ tốn thời gian lâu nhất (hơn 2 lần so với cách một).for (int i = 0; i < 100000; i++){bool b = s1.Equals(s2,StringComparison.OrdinalIgnoreCase);}
2. Xây dựng chuỗi – String và StringBuilder:
Đoạn mã 1:
string str=”";for (int i = 0; i < 10000; i++){str += “a”;}
Đoạn mã 2:
StringBuilder str=new StringBuilder();for (int i = 0; i < 10000; i++){str.Append(“a”);}
Kết quả cho ta thấy đoạn mã một chạy chậm hơn khoảng từ 200 đến 300 lần đoạn mã hai. Nguyên nhân là toán tử + của lớp string sẽ tạo ra một đối tượng string mới trong mỗi lần lặp, trong khi phương thức Append của StringBuilder sẽ nối trực tiếp vào chuỗi hiện tại.
Tuy nhiên cũng cần chú ý điều này có thể ngược lại nếu như bạn cần chuyển chuỗi StringBuilder thành String trong mỗi lần lặp. Tốc độ thực thi của đoạn mã thứ hai sẽ lâu hơn so với đoạn mã một. Hãy kiểm chứng bằng cách chạy thử đoạn mã hai sau khi sửa lại như sau:
Đoạn mã 2 (đã sửa):
StringBuilder str = new StringBuilder();string strRet;for (int i = 0; i < 10000; i++){str.Append(“a”);strRet = str.ToString();}
3. Nối chuỗi – Phương thức Insert() và toán tử +:
Đoạn mã 1:
string str=”";for (int i = 0; i < 10000; i++){str = “string”;str= str.Insert(0, “my “);}
Đoạn mã 2:
Nếu chạy thử vài lần, bạn sẽ nhận thấy rằng đoạn mã thứ hai chạy nhanh gấp đôi đoạn mã một. Tuy nhiên nếu như không khởi tạo lại giá trị của biến str trong mỗi lần lặp, tốc độ của hai đoạn mã này là xấp xỉ nhau.string str=”";for (int i = 0; i < 10000; i++){str = “string”;str = “my ” + str;}
4. Cắt chuỗi – Substring() và Remove():
Đoạn mã 1:
string str;for (int i = 0; i < 10000; i++){str = “string”;str = str.Remove(0, 1);}
Đoạn mã 2:
Sự khác biệt giữa đoạn mã hai so với đoạn mã một là tốc độ nhanh hơn khoảng 2 lần. Tuy nhiên sự khác biệt này sẽ trở nên khó phân biệt nếu như bạn dùng chúng để cắt ở vị trí khác, như là cuối chuỗi chẳng hạn.string str;for (int i = 0; i < 10000; i++){str = “string”;str = str.Substring(1);}
5. Chuyển đối tượng về dạng chuỗi – Format() và ToString();
Đoạn mã 1:
string str = “”;int obj = 1;for (int i = 0; i < 100000; i++){str = String.Format(“{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}”, obj);}
Đoạn mã 2:
Ở đây tôi tạo ra một đối tượng string với giá trị là mười số 1 trong mỗi lần lặp. Cả hai phương pháp này đều chạy khá lâu với số lần lặp là 100000. Tuy nhiên đoạn mã thứ nhất sẽ tốn thời gian gấp gần 5 lần so với đoạn mã thứ hai, mặc dù đoạn mã thứ hai còn sử dụng toán tử cộng chuỗi.string str = “”;int obj = 1;for (int i = 0; i < 100000; i++){string s = obj.ToString();str = s + s + s + s + s + s + s + s + s + s;}
6. Sự khác biệt giữa các phương thức nạp chồng (overloaded):
Bạn có thể không để ý rằng giữa các phương thức nạp chồng, thực hiện cùng một công việc lại có sự khác biệt về khoảng thời gian mà chúng thực thi. Điều này thường thấy trong các phương thức với tham số là một hoặc nhiều kiểu đối tượng được tạo ra từ những thành phần đơn giản hơn.
Chẳng hạn như phương thức khởi tạo của đối tượng Rectangle, ta sẽ thử xét hai phương thức:
- (1) Rectangle (Point location, Size size)
- (2) Rectangle (int x, int y, int width, int height)
Sự khác biệt về thời gian thực thi giữa 2 phương thức phụ thuộc vào đoạn mã chúng ta viết.
Nếu bạn so sánh giữa 2 đoạn mã sau:
Đoạn mã 1:
for (int i = 0; i < 100000; i++){Rectangle rec = new Rectangle(new Point(i,i), new Size(i,i));}
Đoạn mã 2:
Bạn có thể nhận thấy là đoạn mã thứ nhất chạy lâu hơn đoạn thứ hai khoảng 7 lần. Lý do xảy ra điều này bạn có thể đoán được là phương thức thứ nhất phải tạo ra 2 đối tượng trong mỗi lần lặp.for (int i = 0; i < 100000; i++){Rectangle rec = new Rectangle(i,i,i,i);}
7. Hạn chế sử dụng khối try catch:
int a = 0, b = 10;
Đoạn mã 1:
for (int i = 0; i < 1000; i++){try{int c = b / a;}catch { }}
Đoạn mã 2:
Dòng lệnh int c = b / a dĩ nhiên luôn ném ra ngoại lệ Divide by Zero (chia cho 0) nếu được thực thi. Tuy nhiên ngoại lệ này bạn có thể đoán trước và kiểm tra mẫu số phải khác 0 trước khi tiếp tục hay không. Sự cẩn trọng như trong đoạn mã 2 giúp bạn tiết kiệm được thời gian khoảng 30,000 (30 nghìn) lần so với đoạn mã 1.for (int i = 0; i < 1000; i++){if (a == 0)continue;int c = b / a;}
8. Hạn chế việc gọi phương thức:
Giả sử bạn có 1 phương thức Method() đơn giản như sau:
private void Method(){int a = 1, b = 2;int c = a + b;}
Bạn có hai cách để thực thi hai phương thức trên như sau:
Đoạn mã 1:
for (int i = 0; i < 1000000; i++){Method();}
Đoạn mã 2:
Đoạn mã thứ hai chạy nhanh đoạn thứ nhất 3 lần. Các lệnh nhảy mặc dù rất tốn rất ít chi phí nhưng với số lượng nhiều có thể làm chậm tốc độ thực thi của chương trình khá rõ ràng.for (int i = 0; i < 1000000; i++){int a = 1, b = 2;int c = a + b;}
9. Sử dụng cấu trúc thay cho lớp:
Giả sử bạn có một lớp và một cấu trúc có cùng thành viên và thuộc tính đơn giản như sau:
class MyClass{private int value;public int Value{get { return this.value; }set { this.value = value; }}}struct MyStruct{private int value;public int Value{get { return this.value; }set { this.value = value; }}}
Hai đoạn mã cũng thực hiện cùng một nhiệm vụ:
Đoạn mã 1:
for (int i = 0; i < 1000000; i++){MyClass myClass = new MyClass();myClass.Value = 1;}
Đoạn mã 2:
Khi chạy thử bạn có thể nhận thấy đoạn mã thứ hai chạy nhanh hơn khoảng trên 4 lần so với đoạn mã thứ nhất. Lý do chính khá đơn giản là cấu trúc đơn giản và có ít tính năng hơn lớp, vì thế như đã nói trước, bạn chỉ nên sử dụng đối với những kiểu dữ liệu đơn giản, thành viên của nó thường là những kiểu dữ liệu đã được xây dựng sẵn.for (int i = 0; i < 1000000; i++){MyStruct myStruct= new MyStruct();myStruct.Value = 1;}
10. Sử dụng tập hợp (collection) có định kiểu:
11. Mảng (array) và tập hợp (collection) – tốc độ hay sự linh hoạt:
Đoạn mã 1:
List<int> list = new List<int>();for (int i = 0; i < 1000000; i++){list.Add(1);}
Đoạn mã 2:
int[] arr = new int[1000000];for (int i = 0; i < arr.Length; i++){arr[i] = 1;}
Tốc độ thực thi của đoạn mã thứ hai có thể nhanh hơn 4-6 lần đoạn mã một. Tuy nhiên nếu bạn sử dụng ArrayList trong đoạn mã một, sự khác biệt này có tăng lên đến 15 lần. Một lần nữa bạn thấy sự cải thiện đáng kể khi sử dụng tập hợp có định kiểu.
Trên đây chỉ là một vài kinh nghiệm cơ bản, bạn có thể tự tìm hiểu và khám phá thêm trong quá trình thực hành.
No comments:
Post a Comment