Tài trợ bài viết này và giới thiệu dịch vụ, sản phẩm, thương hiệu, nhu cầu tuyển dụng của doanh nghiệp đến với cộng đồng.
La Kiến Vinh Bài viết hướng tối ưu hóa trong lập trình với C++, tối ưu hóa lập trình C++ với games, bài viết hướng games bởi vì games đòi hỏi hiệu năng rất cao, và các games lớn thông thường sử dụng C++ làm nền tảng.
Nội dung bài viết

Giới thiệu

Không chỉ games đòi hỏi hiệu năng cao mà các loại ứng dụng khác cũng vậy, các nhà phát triển rất cần sự đóng góp thêm ý tưởng tối ưu là rất cần thiết.

Đối tượng hướng đến

Các bạn có hiểu biết tốt về kỹ thuật lập trình và khả năng C/C++ tốt.

Bài liên quan

  1. Tư Duy Tối Ưu Hóa Trong Lập Trình Games - Phần 1: Codes Trong C/C++
  2. Tư Duy Tối Ưu Hóa Trong Lập Trình Games - Phần 2: Quản Lý Bộ Nhớ Phân Mảnh
  3. STDIO Coding Convention - Level 1

Các trường hợp tối ưu hóa codes

  • Tạo một đối tượng khi thật sự cần thiết
  • Gọi hàm khởi tạo khi thật sự cần thiết
  • Chỉ định hàm khởi tạo cho các đối tượng là thuộc tính của class
  • Sử dụng tham chiếu, con trỏ (shallow copy) nếu có thể

Tạo một đối tượng khi thật sự cần thiết

Khi thật sự không cần thiết, ta nên hạn chế khởi tạo 1 đối tượng vì chi phí bao gồm hiệu suất cho khởi tạo và thu hồi rất lớn, bên cạnh đó còn tốn bộ nhớ để lưu trữ.

Ta xét trường hợp sau

void BeAStdio()
{
	Stdio sObj;
	
	...
}

Từ đoạn ... trở xuống, ta không hề sử dụng sObj nên đó là 1 sự lãng phí ta phải bỏ. Tuy nhìn đơn giản nhưng rất nhiều bạn vẫn không xóa nó đi. Và chính vì thế cho đến lúc code phức tạp hơn 1 chút như bên dưới

char* BeAStdio(bool enable)
{
	Stdio sObj;
	
	if (enable == true)
		return NULL;
	else
		...
}

Nếu enable == true thì hàm sẽ ngưng thực thi và trả NULL, do đó sObj sẽ không để làm gì cả. Một sự bất cẩn thường xuyên sẽ dẫn đến các bất cẩn đáng tiếc khi mọi thứ phức tạp hơn. Vậy ta cần tạo thói quen tốt, đó là tính tiết kiệm.

Gọi hàm khởi tạo khi thật sự cần thiết

Mặc định ta khởi tạo 1 đối tượng, hàm tạo (constructor) sẽ được gọi sau khi hàm đó được khởi tạo. Giả sử trong hàm khởi tạo có dạng sau

#include <string.h>

typedef char byte;

class StdioMember
{
private:
	byte* m_profilePicture;
	int m_profilePictureLength;
	
	char* m_DNAString;
	int m_DNAStringLength;

public:
	StdioMember(int pictureLength, byte* profilePicture,
				int DNALength, char* DNA)
	{
		m_profilePicture = new byte[pictureLength];
		m_profilePictureLength = new char[DNALength];
		
		memcpy(m_profilePicture, profilePicture, pictureLength);
		memcpy(m_DNAString, DNA, DNALength);
	}
};

int main()
{
	/*......*/

	StdioMember member(picLength, pic, dnaLength, dna);

	/*......*/
	
	return 0;
}

Ngay từ lúc khởi tạo StdioMember member thì constructor sẽ được gọi ngay sau đó và điều thuận tiện này cũng là điều bất lợi, vì đôi lúc đó chưa phải là thời điểm mà ta muốn khởi tạo các giá trị cho thuộc tính trong đối tượng, có khi thời điểm đó ta cũng đang cần hiệu suất của máy tính và muốn việc khởi tạo được diễn ra sau đó (khi nào mà ta muốn), vậy ta không nên đặt các khởi tạo tại constructor.

Nếu chưa cần khởi tạo, ta có 2 cách và thông thường ta sử dụng con trỏ và khi nào cần khởi tạo ta sẽ cấp phát động bằng toán tử new. Nhưng giả sử bài toán đặt ra là phải cấp phát đối tượng nhưng chưa cần khởi tạo các giá trị trong đối tượng thì ta sẽ thiết kế lại lớp như sau.

  • Thêm 1 constructor với thân của constructor là rỗng.
  • Viết thêm 1 phương thức khởi tạo và ta sẽ gọi phương thức đó khi nào ta thấy cần thiết.

Vậy ta được code như sau

#include <string.h>

typedef char byte;

class StdioMember
{
private:
	byte* m_profilePicture;
	int m_profilePictureLength;
	
	char* m_DNAString;
	int m_DNAStringLength;

public:

	StdioMember()
	{
	}
	
	void Init(int pictureLength, byte* profilePicture,
				int DNALength, char* DNA)
	{
		m_profilePicture = new byte[pictureLength];
		m_profilePictureLength = new char[DNALength];
		
		memcpy(m_profilePicture, profilePicture, pictureLength);
		memcpy(m_DNAString, DNA, DNALength);
	}
};

int main()
{
	/*......*/

	StdioMember member;

	/*......*/
	
	member.Init();
	
	return 0;
}

Chỉ định hàm khởi tạo cho các đối tượng là thuộc tính của class

Với 1 thuộc tính trong 1 class, có thể đó là 1 đối tượng, nếu như khai báo thông thường ta xem như đã gọi constructor mặc định cho nó, và không có cách thức để gọi các constructor mà ta mong muốn.

class Sins
{
private:
	string m_name;
	
public:
	Sins(string & name)
	{		
		m_name = name;
	}
}

Phân tích codes trên ta có, 2 quá trình: Sins được tạo ra, Sins gọi constructor.

  • Trong quá trình đối tượng Sins được tạo thành thì m_name được tạo. Phân tích riêng m_name được tạo, nó sẽ gọi constructor của nó.
  • Vào thân hàm constructor của Sins và xảy ra copy-constructor gắn name vào m_name.

Xét riêng m_name ta thấy nó phải gọi constructor trước, sau đó lại tiến hành gán (gọi copy-construtor) thêm 1 lần nữa. Vậy ta nghĩ đến vấn đề, tại thời điểm khởi tạo m_name ta sẽ gọi thẳng copy-constructor để tránh thêm bước gọi constructor.

Để làm điều này, ta sẽ chỉ định copy-constructor cho Sins khi nó đang khởi tạo bằng cách sau

class Sins
{
private:
	string m_name;
	
public:
	Sins(string & name):m_name(name)
	{
	}
}

Tôi có giải thích thêm về vấn đề này trong bài viết Sự Ra Đời Của Phương Thức Khởi Tạo (Constructor), Phương Thức Hủy (Destructor) Trong Một Class - C++.

Sử dụng tham chiếu, con trỏ nếu có thể

Với C, ta chỉ có con trỏ, bản chất khi truyền con trỏ vào 1 hàm là gán địa chỉ của con trỏ, nếu con trỏ đang quản lý 1 đối tượng, ta không tốn kém việc truyền các đối tượng vào hàm. Đối với cách truyền tham chiếu (tronng C++) cũng có ý tưởng tương tự như vậy.

Như hàm vẽ 1 đối tượng tên là Sins bên dưới, ta không cần thiết phải sao chép cả đối tượng, vì trong trường hợp này ta chỉ muốn lấy dữ liệu từ nó để vẽ (bạn có thể sử dụng phương pháp truyền địa chỉ qua pointer nếu muốn, nhưng ở đây tôi dùng phương pháp tên chung - truyền tham chiếu).

void DrawSins(Sins & s, int posX, int posY)
{
	Draw(s.texture, s.rect.x, s.rect.y, 
					s.rect.width, s.height,
					posX, posY);
}
THẢO LUẬN
ĐÓNG