Lưu Quang Triệu

Không ngừng sáng tạo thì sẽ không sợ bị diệt vong

Các vấn đề ảnh hưởng đến hiệu suất và tính khả mở

Posted by millionking on January 10, 2012

Bộ nhớ:
Việc bạn tạo quá nhiều object, gặp lỗi trong việc giải phóng tài nguyên, cấp phát lại bộ nhớ cho các object đã được cấp phát hay bắt ép GC phải giải phóng bộ nhớ đồng nghĩa với việc bạn ngăn cản CLR quản lý tốt bộ nhớ. Điều này dẫn đến việc tăng khối lượng công việc phải thực hiện và giảm hiệu suất của ứng dụng.

Dọn dẹp tài nguyên
Gọi Finalizer khi không cần thiết, gặp lỗi khi ngăn chặn quá trình Finalization trong method Dispose hay gặp lỗi trong việc giải phóng các “unmanaged resource” cũng dẫn đến những chậm trễ không cần thiết trong việc lấy lại tài nguyên đã cấp phát. Nó cũng tiềm ẩn khả năng tạo ra “resource leak” (bạn không thể thu hồi lại tài nguyên sau khi đã cấp phát).

Sử dụng không đúng các thread
Việc tạo thread cho từng request và không chia sẻ các thread sử dụng thread pool sẽ dẫn đến hiện tượng “bottleneck” (thắt nút cổ chai) đối với hiệu suất và tính khả mở của các ứng dụng, đặc biệt là các ứng dụng server. .NET Framework cung cấp khả năng tự điều chỉnh thread pool, bạn nên sử dụng chúng trong các ứng dụng phía server.

Lạm dụng các tài nguyên dùng chung
Việc cấp phát tài nguyên cho từng request có thể dẫn đến việc bạn tốn quá nhiều tài nguyên, việc gặp lỗi khi giải phóng các tài nguyên dùng chung cũng là chậm quá trình thu hồi chúng. Điều này nhanh chóng dẫn đến những rắc rối về tính khả mở của ứng dụng.

Chuyển đổi kiểu
Việc chuyển đổi kiểu không tường mình và việc trộn lẫn giữa các kiểu tham trị (value) với các kiểu tham chiếu (reference) sẽ dẫn đến những xử lý boxing và unboxing không cần thiết.

Lạm dụng collection
.NET Framework cung cấp tập hợp các kiểu collection khách nhau. Mỗi collection được thiết kế để sử dụng đối với từng yêu cầu lưu trữ và truy xuất khác nhau. Việc lựa chọn sai kiểu collection đối với những tình huống xác định sẽ ảnh hưởng đến hiệu suất ứng dụng.

Các vòng lặp không hiệu quả
Việc coding dù chỉ không hiệu quả một chút cũng sẽ bị phóng đại lên gấp nhiều lần khi nó được đặp trong một vòng lặp. Các vòng lặp có truy xuất đến các thuộc tính (property) của đối tượng (object) thường là thủ phạm của hiện tượng “bottleneck” (nút cổ chai), đặc biệt nếu như đối tượng là remote hay property phải xử lý khá nhiều công việc bên trong.

Cân nhắc đến thiết kế của ứng dụng

Quản lý tài nguyên phải được thiết kế để thực thi hiệu quả
Tránh cấp phát các đối tượng và tài nguyên trước khi bạn cần đến chúng và đảm bảo bạn giải phóng chúng ngay sau khi bạn không còn sử dụng. Lời khuyên này được áp dụng với các kiểu tài nguyên như database connection, data reader, file, stream, network connection, COM object. Sử dụng finally hoặc using (C#) để đảm bảo tài nguyên được đóng hoặc giải phóng trong bất kì tình huống nào, thậm chí là khi có exception. Lưu ý rằng sử dụng từ khóa using trong C# chỉ áp dụng với những object nào implement IDisposable interface trong khi finally có thể sử dụng với bất kì kiểu đối tượng nào.

Giảm những liên kết ràng chéo
Mục đích là giảm số lượng lời gọi đến các method giữa các “remoting boundary” khác nhau bởi vì chúng sử dụng marshaling và có thể dẫn đến việc quá tải khi phải chuyển đổi giữa các thread. Bên trong managed code có khá nhiều loại remoting boundary khác nhau:

Application domain:
Đây là boundary hiệu quả nhất khi thực hiện những lời gọi từ boundary khác (cross) bởi vì nó nằm bên trong ngữ cảnh (context) của một process duy nhất. Bởi vì thời gian để thực hiện một lời gọi là rất thấp nên độ phức tạp của việc thực hiện cross được tính dựa trên số lượng, kiểu và kích thước các tham số của hàm.

Process:
Ảnh hưởng nhẹ đến hiệu suất. Vì vậy chỉ sử dụng khi thực sự cần thiết.

Machine:
Những lời gọi cross-machine cực kì tốn kém vì nó phải thực hiện marshaling và phải dựa trên hệ thống network.

Unmanged code:
Bạn nên cân nhắc đến khả năng này vì nó cũng cần thực hiện marshaling và có thể đặt gánh nặng lên CPU khi phải chuyển đổi giữa các thread. Platform Invoke (P/Invoke) và COM interop layer của CLR hoạt động rất hiệu quả nhưng hiệu suất vẫn phụ thuộc nhiều vào kiểu và kích thước dữ liệu cần được marshal giữa managed code và unmanaged code.

Một assembly lớn hiệu quả hơn là nhiều assembly nhỏ

  • Việc load metadata cho nhiều assembly nhỏ sẽ tốn kém hơn
  • CLR phải tạo nhiều memory page hơn trong pre-compiled image của CLR để load assembly (nếu các assembly được compile trước sử dụng Ngen.exe)
  • Tốn nhiều thời gian hơn cho quá trình JIT (just-in-time: compile từ IL sang native code)
    Tốn nhiều tài nguyên cho security check hơn.

Tất nhiên, điều này không đồng nghĩa việc bạn sẽ có một assembly duy nhất cho ứng dụng. Nhưng hãy đặc các class của mình đúng chỗ để có được hiệu suất cao nhất.

Quản lý code
Hãy xem xét cách bạn thiết kế các class và cách bạn phân chia code thành các method. Khi code được phân chia tốt, sẽ dễ dàng hco bạn hơn để tăng performance, bảo trì hay thêm các chức năng mới. Tuy nhiên, công việc này đòi hỏi sự cân bằng. Việc phân chia có thể làm dễ dàng trong quá trình bảo trì nhưng bạn có thể sẽ lúng túng trước những lớp trừu tượng (abstraction) và việc phải tạo quá nhiều layer. Một thiết kế đơn giản có thể sẽ hiệu quả hơn.

Sử dụng các thread như những tài nguyên dùng chung
Tránh tạo thread cho từng request. Bạn nên hạn chế việc tạo các thread, sử dụng chúng như những tài nguyên dùng chung nhờ thread pool của .NET Framework.

Quản lý exception hiệu quả
Cái giá đối với performance của các exception là rất lớn. Mặc dù việc sử dụng cấu trúc try…catch..finally được khuyến cáo nhưng hãy đảm bảo bạn chỉ sử dụng đối với những trường hợp ngoại lệ không thể biết trước.

Thiết kế class

  • Không tạo các class là “thread safe” mặc định (static class nên mặc định là thread safe – tham khảo thêm thông tin về multi-thread)
  • Sử dụng từ khóa sealed khi bạn không muốn user thừa kế class
  • Cân nhắc việc sử dụng virtual method. Nó mang lại khả năng mở rộng, đồng thời cũng ảnh hưởng đến performance.
  • Cân nhắc việc sử dụng các overload method với một method có số lượng tham số không cố định (từ khóa params)
  • Cân nhắc việc override lên method Equals với các kiểu tham trị (value type)
  • Tránh truy xuất liên tục đến property (sử dụng local variable để lưu lại giá trị này)
  • Cân nhắc việc sử dụng private và public. Hạn chế dùng public nếu nó không thực sự cần thiết.
  • Tránh sử dụng từ khóa volatile vì nó ép buộc truy xuất đến một biến thông qua địa chỉ bộ nhớ thay vì một thanh ghi (register) đã được load nội dung.

Garbage Collection (GC)

  • Tránh gọi GC.Collect, nó tiêu tốn thời gian do GC phải duyệt qua mọi object. Nếu bạn buộc phải gọi method này, hãy cân nhắc 2 điều sau:
    • Gọi GC.WaitForPendingFinalizers sau khi gọi GC.Collect để đảm bảo thread sẽ đợi cho đến khi finalizer của các object đều được gọi
    • Sau khi các finalizer được gọi, sẽ có rất nhiều object không còn cần thiết. Một lời gọi đến GC.Collect sẽ thu dọn các object này.
  • Sử dụng WeakReference cho việc cache lại dữ liệu
  • Tránh việc promotion (đưa các objec từ Generation thấp lên thành Generation cao) các Short-lived object (các object được cấp phát và bị thu hồi trước khi quay lại Gen(eration) 0 được gọi là Short-lived object) bằng cách:
    1. Không tham chiếu short-lived object đến long-lived object. Ví dụ:
      class Customer{
      Order _lastOrder;
      void insertOrder (int ID, int quantity, double amount, int productID){
      Order currentOrder = new Order(ID, quantity, amount, productID);
      currentOrder.Insert();
      this._lastOrder = currentOrder; //Promote short-lived object _lastOrder
      }
      }
    2. Tránh gọi Finalize method: GC phải promote các object có thể finalize để thực hiện, do đó các object này trở thành long-lived object
    3. Tránh để các object có thể finalize tham chiếu đến các object khác. Nó sẽ khiến những object này trở thành long-lived object.
  • Gán các biến không còn cần sử dụng đến giá trị null trước khi thực hiện một lời gọi tiêu tốn thời gian (không cần áp dụng với các biến cục bộ do JIT compiler có thể tự quyết định khi nào biến này không còn được reference nữa)
  • Hạn chế việc cấp phát không rõ ràng (ví dụ sử dụng String.Split để tạo một array of string)
  • Tránh hoặc hạn chế sử dụng các Complex Object Graphs (các object reference đến nhiều object khác).
  • Tránh cấp phát bộ nhớ trước khi bạn sử dụng

Finalize và Dispose

  • Finalize được gọi trước khi bộ nhớ bị thu hồi. Do đó, bạn có thể thu hồi các unmanaged resource đã được cấp phát.
  • Dispose được cung cấp cho các kiểu có chứa reference đến các object khác và bạn muốn giải phóng chúng thông qua việc gọi method này. Bạn nên implement IDisposable interface cho phép consumer (nơi sử dụng object) gọi đến method này. Qua đó, bạn có thể tránh việc finalization vì nó cần nhiều thời gian để giải phóng hòan toàn các tài nguyên managed và unmanaged.
  • Để cấm GC không thực hiện finalizatio, bạn có thể gọi method GC.SuppressFinalization.
    Dưới đây là một Dispose pattern chuẩn được khuyến cáo:

public sealed class MyClass : IDisposable
{
// Variable to track if Dispose has been called
private bool disposed = false;
// Implement the IDisposable.Dispose() method
public void Dispose()
{
// Check if Dispose has already been called
if (!disposed)
{
// Call the overridden Dispose method that contains common cleanup code
// Pass true to indicate that it is called from Dispose
Dispose(true);
// Prevent subsequent finalization of this object. This is not needed
// because managed and unmanaged resources have been explicitly released
GC.SuppressFinalize(this);
disposed = true;
}
}

// Implement a finalizer by using destructor style syntax
~MyClass()
{
// Call the overridden Dispose method that contains common cleanup code
// Pass false to indicate the it is not called from Dispose
Dispose(false);
}

// Implement the override Dispose method that will contain common
// cleanup functionality
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Dispose time code
}
else
{
// Finalize time code
}
}
}

  • Nếu bạn implement finalization, hãy implement IDisposable interface
  • Bạn chỉ nên implement finalzation nếu bạn cần giải phóng các unmanaged resource.
  • Nên gọi GC.SuppressFinalize bên trong Dispose method
  • Tránh gọi Dispose method nhiều lần
  • Nếu bạn thừa kế từ một “disposable class”, hãy gọi base.Dispose() bên trong Dispose method. Nếu bạn sử dụng bất kì object nào implement IDisposable interface, bạn cũng phải gọi Dispose method của object đó bên trong Dispose method.
  • Giữ code của Finalize method đơn giản để ngăn chặn lỗi (điều này sẽ dẫn đến việc tài nguyên không thể giải phóng).

Value Type và Reference Type

Mọi kiểu dữ liệu trong .NET hoặc là value type (tham trị), hoặc là reference type (tham chiếu). Phần này giới thiệu cho bạn hai vấn đề cơ bản về kiểu dữ liệu.

Value Type

  • Kiểu liệt kê – Enum
  • Dữ liệu cấu trúc – Struct
  • Các kiểu dữ liệu nguyên thủy – Primitive type (Boolean, Date, Char)
  • Dữ liệu số như Decimal
  • Số nguyên (Byte, Short, Integer, Long)
  • Số thực (Single, Double)

Reference Type

  • Class
  • Delegate
  • Exception
  • Attribute
  • Array

Value Types
Bộ nhớ dành cho value type được cấp phát bên trong stack của thread hiện tại. Dữ liệu của value type được lưu hoàn toàn bên trong vùng bộ nhớ này, tồn tại cũng thời gian tồn tại của stack. Dữ liệu của value type có thể tồn tại lâu hơn thời gian tồn tại của stack nếu như nó được tạo ra bằng cách truyền qua tham số của hàm hay gán đến một kiểu tham chiếu. Value type mặc định luôn được truyền bởi giá trị. Nếu một value type được dùng như một tham số của reference type, một “wrapper object” sẽ được tạo ra (boxing), sau đó dữ liệu của value type sẽ được copy đến “wrapper object”.

Reference Types
Ngược lại với value type, dữ liệu của reference type luôn được lưu trong managed heap. Các biến là reference type chỉ lưu một pointer (con trỏ) đến dữ liệu này. Vùng bộ nhớ dành cho reference type (class, delegate, exception…) được thu hồi bởi GC khi chúng không còn được tham chiếu. Một điểm quan trọng cần lưu ý là reference type luôn luôn được truyền bởi reference (truyền địa chỉ của nó). Nếu bạn muốn một reference type được truyền như là value type, một bản copy của reference type sẽ được tạo ra và dùng như là đối số mong muốn.

Boxing và Unboxing

Uhm, chúng ta đang nói đến một khái niệm của .NET chứ không phải môn quyền anh

Bạn có thể chuyển một value type thành reference type và ngược lại. Khi một biến là value type cần được chuyển thành reference type, một object sẽ được cấp phát trên managed heap và giá trị của biến value type sẽ được copy đến object này (object này có thể coi như là một “cái hộp” – box). Quá trình này gọi là boxing. Boxing có thể là tường minh hoặc không tường minh.

int p = 123;
Object box;
box = p; // Không tường minh
box = (Object)p; // Tường minh

Boxing thường xảy ra khi bạn truyền một value type đến một method yêu cầu tham số là một Object. Khi giá trị một object cần được chuyển ngược lại thành value type, dữ liệu của nó sẽ được copy đến một vùng lưu trữ phù hợp. Quá trình này gọi là Unboxing.

p = (int)box; // Unboxing

Boxing có thể dẫn đến những vấn đề về performance khi đặt bên trong lệnh lặp (chẳng hạn một collection các value type)

  • Tránh boxing và unboxing quá thường xuyên
    Boxing và Unboxing liên quan đến quá trình copy giữa managed heap với stack, vì vậy nếu quá trình này diễn ra quá thường xuyên sẽ ảnh hướng đến performance.
  • Collection và Boxing
    Một số collection chứa kiểu dữ liệu cơ bản là Object. Lưu value type vào những collection này sẽ dẫn đến quá trình boxing. Trong những trường hợp này, cân nhắc việc sử dụng mảng (array) hoặc các kiểu collection tự tạo (custom collection).
    (*) Với .NET 2.0, bạn có thể cân nhắc sử dụng các generic collection như List<T>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: