Chuyện tối ưu code, xấu đẹp, đẹp xấu

Chuyện tối ưu code, viết code cho thật đẹp là công việc hàng ngày của mỗi lập trình viên, điều đó ai cũng biết. Nhưng liệu code tối ưu có phải là code đẹp, và ngược lại? Đây là câu hỏi mà chắc hẳn nhiều bạn đều thắc mắc. Tôi sẽ kể một câu chuyện, và kết luận thế nào thì các bạn vui lòng kéo xuống phần “Kết luận” để đọc nhé.

Vậy câu chuyện là gì?

Chuyện là một đêm Sài Gòn mát lạnh (do tôi ở trong phòng mở máy lạnh, chứ ngoài trời thì tôi không biết), tôi rảnh rỗi ngồi lướt Github (thay vì lướt Facebook) thì tình cờ tôi đọc được một đoạn code khá là thú vị.

vec4 tex00 = texture2D(textureSampler, texCoords + vec2(-off2.x, -off2.y));
vec4 tex10 = texture2D(textureSampler, texCoords + vec2(-off.x, -off2.y));
vec4 tex20 = texture2D(textureSampler, texCoords + vec2(0.0, -off2.y));
vec4 tex30 = texture2D(textureSampler, texCoords + vec2(off.x, -off2.y));
vec4 tex40 = texture2D(textureSampler, texCoords + vec2(off2.x, -off2.y));
vec4 tex01 = texture2D(textureSampler, texCoords + vec2(-off2.x, -off.y));
vec4 tex11 = texture2D(textureSampler, texCoords + vec2(-off.x, -off.y));
vec4 tex21 = texture2D(textureSampler, texCoords + vec2(0.0, -off.y));
vec4 tex31 = texture2D(textureSampler, texCoords + vec2(off.x, -off.y));
vec4 tex41 = texture2D(textureSampler, texCoords + vec2(off2.x, -off.y));
vec4 tex02 = texture2D(textureSampler, texCoords + vec2(-off2.x, 0.0));
vec4 tex12 = texture2D(textureSampler, texCoords + vec2(-off.x, 0.0));
vec4 tex22 = texture2D(textureSampler, texCoords + vec2(0.0, 0.0));
vec4 tex32 = texture2D(textureSampler, texCoords + vec2(off.x, 0.0));
vec4 tex42 = texture2D(textureSampler, texCoords + vec2(off2.x, 0.0));
vec4 tex03 = texture2D(textureSampler, texCoords + vec2(-off2.x, off.y));
vec4 tex13 = texture2D(textureSampler, texCoords + vec2(-off.x, off.y));
vec4 tex23 = texture2D(textureSampler, texCoords + vec2(0.0, off.y));
vec4 tex33 = texture2D(textureSampler, texCoords + vec2(off.x, off.y));
vec4 tex43 = texture2D(textureSampler, texCoords + vec2(off2.x, off.y));
vec4 tex04 = texture2D(textureSampler, texCoords + vec2(-off2.x, off2.y));
vec4 tex14 = texture2D(textureSampler, texCoords + vec2(-off.x, off2.y));
vec4 tex24 = texture2D(textureSampler, texCoords + vec2(0.0, off2.y));
vec4 tex34 = texture2D(textureSampler, texCoords + vec2(off.x, off2.y));
vec4 tex44 = texture2D(textureSampler, texCoords + vec2(off2.x, off2.y));
vec4 tex = tex22;
// Blur
vec4 blurred = 1.0 * tex00 + 4.0 * tex10 + 6.0 * tex20 + 4.0 * tex30 + 1.0 * tex40
+ 4.0 * tex01 + 16.0 * tex11 + 24.0 * tex21 + 16.0 * tex31 + 4.0 * tex41
+ 6.0 * tex02 + 24.0 * tex12 + 36.0 * tex22 + 24.0 * tex32 + 6.0 * tex42
+ 4.0 * tex03 + 16.0 * tex13 + 24.0 * tex23 + 16.0 * tex33 + 4.0 * tex43
+ 1.0 * tex04 + 4.0 * tex14 + 6.0 * tex24 + 4.0 * tex34 + 1.0 * tex44;
blurred /= 256.0;
view raw index.glsl hosted with ❤ by GitHub

Cảm giác ban đầu của các bạn thế nào? Nhìn khó chịu đúng không? Tôi cũng vậy.

Để giới thiệu qua đoạn code này một tí. Đây là đoạn code viết bằng OpenGL Shading Language, có thể giới thiệu đại khái đây là ngôn ngữ cấp cao (high level language) dành cho việc lập trình các shader trong OpenGL (cái này chắc các bạn lập trình game với OpenGL hiểu rõ hơn, các bạn giúp mình giải thích rõ hơn trong phần comment nhé).

Đoạn code này để làm việc gì?

Đoạn code này được trích ra từ một ứng dụng xem ảnh trên web (tương tự Google Photo), và nhiệm vụ của nó là tính giá trị màu sắc của một điểm ảnh sau khi áp dụng filter, cụ thể ở đây là blur (làm mờ).

Thuật toán đằng sau nó là gì?

Trước khi đi sâu vào đoạn code đó, chúng ta cùng nhìn qua về mặt giải thuật. Để tính được giá trị màu sắc của một điểm trên ảnh sau khi áp dụng filter chúng ta không thể chỉ thay đổi giá trị màu sắc của chính điểm đó bằng cách cộng trừ nhân chia với một giá trị delta nào đó, bởi vì làm vậy thì màu sắc của từng điểm sẽ dễ dàng bị khác biệt và tạo ra cảm giác không đẹp, vì bức ảnh là một tổ hợp của rất nhiều điểm.

Cách giải quyết mà người ta áp dụng vô trường hợp này là thay vì chỉ lấy giá trị màu của một điểm, người ta lấy thêm giá trị màu của những điểm xung quanh nó, cụ thể ở đây là lấy điểm muốn thay đổi màu sắc là tâm và tạo thành một ma trận 5×5 xung quanh nó (như vậy tổng cộng ta lấy 25 điểm), thật ra lấy bao nhiêu điểm cũng được, có thể là 3×3, 7×7, có thể ở trường hợp này 5×5 là giá trị cho ra bức ảnh đẹp nhất. Nghe khó hiểu đúng không, tôi có vẽ hình minh hoạ đây.

sketch.png

Điểm tôi khoanh màu đỏ chính là điểm cần tính và nó có trọng số cao nhất, và những điểm xung quanh có trọng số như tôi viết trong hình. Sau khi đã tính toán được tổng thì ta chia tổng cho 256 để ra giá trị màu thực tế, vì gía trị màu chỉ nằm trong khoảng từ 0 đến 1, và 256 là tổng trọng số của ma trận.

Tương ứng như giải thích ở trên của tôi thì đoạn code bên trên khai báo từ tex00 đến tex44, chính là đoạn code lấy giá trị màu của từng điểm trong ma trận, và bên dưới đó chính là phép tính tổng theo trọng số.

Các bạn hiểu hết ý nghĩa của đoạn code rồi đúng không? Nếu chưa hiểu thì đọc lại lần nữa nhé. Vấn đề chúng ta dễ dàng thấy được trong đoạn code này đó là việc lấy giá trị màu bị lặp đi lặp lại nhiều lần và có thể giải quyết bằng một đoạn code ngắn hơn với việc sử dụng vòng lặp (bao nhiêu bạn nghĩ như vậy khi đọc tới đây thì nhớ comment nhé).

Tôi sẽ viết ví dụ bằng JavaScript cho dễ nhé (vì tôi cũng không rành GLSL lắm). Tương ứng với cách giải quyết bài toán như trên ta có đoạn code sau.

// input (x, y)
const weights = [
[1, 4, 16, 4, 1],
[4, 16, 24, 16, 4],
[16, 24, 36, 24, 16],
[4, 16, 24, 16, 4],
[1, 4, 16, 4, 1],
];
let blur = 0;
let totalWeight = 0;
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
const weight = weights[i][j];
totalWeight += weight;
blur += weight * getPixelColor(x + i - 2, y + j -2);
}
}
blur /= totalWeight;
view raw index.js hosted with ❤ by GitHub

Đoạn code này có gì? Như các bạn thấy tôi khai báo một mảng 2 chiều, tượng trưng cho trọng số của từng điểm trong ma trận. Sau đó tôi dùng vòng lặp để tính toán. Rõ ràng đoạn code như thế này gọn gàng và đẹp đẽ hơn rất nhiều so với việc trải thẳng nó ra như trên.

Câu hỏi đặt ra là code như tôi có tối ưu không?

Câu trả lời là không.

Tại sao?

Quay lại câu chuyện đoạn code bên trên (tức là đoạn code GLSL đó) là đoạn code tương tác đồ hoạ, và được chạy trên GPU, đây chính là vấn đề.

Ủa chứ chạy trên GPU thì sao?

Để nói câu chuyện này, chúng ta phải hiểu sơ qua về cách mà một cái card đồ hoạ hoạt động. Một cái card đồ hoạ sẽ có rất nhiều GPU, và GPU thì làm việc tính toán là tốt nhất. Và việc xử lý rẽ nhánh, câu điều kiện là chuyện mà nó làm không hề tốt bằng CPU, và nhiều khi còn tệ nữa, bởi vì cấu trúc vi xử lý khác nhau.

Trong điều kiện lý tưởng GPU giống như là một đường ống (pipeline) chỉ làm nhiệm vụ là nhận vào input, tính toán, và xuất ra kết quả mà không quan tâm đến việc quyết định nên làm cái gì. Thực tế, vẫn có một số ít card đồ hoạ vẫn optimize chuyện này ở thời điểm compile time, khi thấy vòng lặp nó sẽ tự động trải ra. Nhưng phần lớn các card đồ hoạ đều coi vòng lặp như là một tác vụ rẽ nhánh và phần lớn sẽ chạy rất chậm. Việc trải các câu lệnh ra, là cách mà chúng ta tối ưu, để cho GPU làm việc mà nó làm tốt nhất đó là tính toán, đọc giá trị màu của điểm input.

Như vậy việc tôi vừa nghĩ tới chuyện rút ngắn code bằng vòng lặp là điều vớ vẩn, và chẳng may không chịu tìm hiểu, mà cứ bang bang vào sửa code tạo Pull Request thì có ngày bị ăn chửi.

Kết luận

Việc code đẹp là code tối ưu, hay code xấu là code không tối ưu đều chỉ là tương đối. Để tối ưu code đôi khi ta còn phải xem xét đến rất nhiều yếu tố khác như trong bài viết này đó là đoạn code đó được thực thi ở đâu. Và trên hết, để kết luận được một điều gì đó các bạn hãy cố gắng tìm hiểu thật kĩ trước khi đưa ra bất cứ kết luận gì.

Nhớ like fanpage hay follow blog nhé, sẽ có nhiều bài viết nữa chờ đón các bạn trong tương lai đó. Và nếu có bất cứ ý kiến hay câu hỏi gì nhớ comment hay hỏi tôi trực tiếp trên fanpage nhé. Chào tạm biệt.

Một suy nghĩ 10 thoughts on “Chuyện tối ưu code, xấu đẹp, đẹp xấu

  1. anh ơi. trong trường hợp nó lớn hơn 5×5 thì số lượng biến cần tạo ra quá nhiều thì liệu cứ khai báo từng biến có còn hợp lí nữa không ạ ?

    1. Đôi lúc mình thấy việc comment rất chi là không cần thiết, nếu comment để gen ra docs thì không nói làm gì. Còn bình thường thì cứ dùng tên biến tên hàm, .. ý nghĩa rõ ràng(cho dù dài loàng ngoàng cũng được). Chứ comment nhiều quá đôi khi lại bị rối khi đọc code

  2. Bài viết khá hay. theo như mình thấy thì ngoài việt tối ưu code thì việc trình bày rất quan trong. quan trọng không phải đơn giản là chỉ nhìn đẹp. mà vấn đề là có thể tìm kiếm cũng như sửa code được dễ dàng hơn. và mình vẫn đang có gắng để tối ưu code sao cho thật tốt.

  3. optimize code bằng for thì chỉ nhìn đẹp hơn thôi anh, khi optimize e nghĩ cần cân nhắc hết các yếu tố như : memory usage, code, hardware endurance, power consumption, performance. .. Mà tốt nhất là hạn chế dùng for. ” We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.”- Donald Knuth

    1. Trong trường hợp này câu bạn trích dẫn ko đúng nữa. Đúng là trong bài viết cái thay đổi này theo cảm quan của ta nó rất nhỏ nhặt nhưng nếu suy nghĩ kĩ 1 bức ảnh bh thường có kích thước lớn tầm vài trăm nghìn đến triệu pixel. Đoạn code trên chạy với số lần bằng số pixel vậy tối ưu nó nhanh hơn chút cho mỗi lần chạy là cực kì quan trọng. Chưa kể ví dụ như trong game ngta thường muốn đạt 60 FPS thì số lần đoạn code trên chạy trong 1s cỡ chục triệu lần

    2. Xin lỗi bạn mình hiểu sai ý cmt của bạn. Mình ko tìm thấy nút xoá cmt nên đành xin lỗi do mình tưởng bạn đang nói tối ưu bằng cách viêt hết ra chứ ko dùng vòng for là ko quan trọng

Bình luận