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 Trong bài này, tôi không cung cấp một giải pháp mà tôi đã từng ứng dụng có liên quan tới việc tính toán các góc lượng giác trong lập trình - tính trước các giá trị, ngoài ra các bạn có thể mở rộng phương pháp tính trước này cho các công việc khác.
Nội dung bài viết

Giới thiệu

Hiện tại việc tính toán lượng giác đã có các thư viện toán học hỗ trợ dường như đầy đủ. Có thể kể trong ngôn ngữ C ta có <math.h>, trong Java ta có java.lang.Math. Trong bài này, tôi không cung cấp một kiến thức nào quá khó để hiểu mà nhằm cung cấp một giải pháp mà tôi đã từng ứng dụng có liên quan tới việc tính toán các góc lượng giác.

Tiền đề bài viết

Thời điểm năm 2009, tôi được sở hữu chiếc điện thoại khá mạnh đầu tiên là K800, tôi đã thử nghiệm viết game với J2ME bằng Java, thông qua việc tìm hiểu tối ưu thì trong đó có một vấn đề làm tôi quan tâm đó là việc tính toán lượng giác quá nhiều làm chậm game của mình và tôi phải tìm một phương pháp để tối ưu. Tôi gọi phương pháp này là TÍNH TRƯỚC (trước khi thời điểm xử lý mà cần giảm việc xử lý quá nhiều của bộ/vi xử lý).

Bạn có thể tự áp dụng giải pháp này cho các vấn đề nảy sinh khác, nhưng ở bài viết này tôi chỉ đề cập nhanh chóng về việc tính toán Sin - Cos - Tang - Cotang (sine, cosine, tangent, cotangent).

ss_1

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

Đây là một giải pháp đơn giản, dễ làm nên bạn chỉ cần biết về lập trình đơn giản có thể áp dụng được.

Tính toán trước là gì?

Việc tính toán trước này hiển hiện trong rất nhiều ứng dụng, game và ranh giới cũng không hề rõ ràng, ta không có khái niệm cụ thể nhưng có thể xem một số ví dụ sau để tiếp cận nhanh.

Trong lúc xử lý giả sử ta gọi hàm sin(α) hàm này sẽ trả về một số thực và dĩ nhiên nó phải được tính toán mới ra được kết quả là con số thực đó, tôi không hiện thực hàm này nên không đánh giá việc thư viện chuẩn họ dùng phương pháp gì để hiện thực, nhưng có thể giả sử hàm sin được hiện thực bằng công thức sau (n càng lớn thì độ chính xác càng cao nhưng thời gian tính toán càng lâu).

ss_2

Chính vì mỗi lần cần tính toán, ta gọi hàm tính toán lại như vậy nên nó chiếm 1 phần xử lý.

Có những kết quả, ta không biết trước do sự thay đổi các nhân tố BẤT KỲ do đó, phải đợi cho đến khi gặp trường hợp cụ thể ta mới cho ra kết quả. Nhưng nếu như nó không là bất kỳ nữa mà đã được giới hạn lại ví dụ ta biết rằng trong yêu cầu phần mềm, ta tính sin của 1 góc với giới hạn là từ 0 độ cho tới 359 độ và các giá trị của góc là số nguyên, ta có thể có 1 giải pháp đó là TÍNH TRƯỚC 360 độ này chứ không để khi cần giá trị phải thực thi lại phép tính toán.

Hiện thực "hàm" sin cho 360 độ

Ngôn ngữ sử dụng cho demo

C/C++ ngoài ra tùy theo dự án của các bạn có thể chọn ngôn ngữ phù hợp với dự án của bạn, cách hiện thực cũng tương tự.

Ý tưởng

Tạo một mảng có 360 phần tử kiểu số thực (có thể là float, double, hoặc bất kỳ kiểu dữ liệu thực nào mà bạn mong muốn)

static double s_sin_value[360] = {...};

Lần lượt gán các giá trị cho từng phần tử sao cho tương ứng phần tử thứ 0 chính là giá trị tại sin của 0 độ, phần tử thứ 1 sẽ có giá trị là giá trị của sin 1 độ, và cứ thế cho đến phần tử thứ 359.

static double s_sin_value[360] =
{
	0.0,			// Phần tử thứ 0
	0.017452406437284660,
	0.034899496702503266,
	......,
	1.0,			// Phần tử thứ 90
	......,
	-0.017452406436871508	// Phần tử thứ 359
};

Xây dựng hàm sin để lấy giá trị từ mảng đã cho trước, chúng ta cần lưu ý rằng, bất kỳ góc nào (lớn hơn 359 độ hoặc nhỏ hơn 0 độ) phải được chuyển về phạm vi từ 0 độ đến 359 độ, do lượng giác của các góc này có tính lặp lại. sin của 360 độ chính là sin của 0 độ. Tuy nhiên, ta có thể làm cho mọi việc đơn giản hơn bằng cách bắt buộc các client code phải sử dụng theo hướng truyền vào từ 0 độ đến 359 độ (đây là sự thỏa hiệp riêng) và tôi không đề cập ở đây. Tôi cũng không thỏa hiệp mà sẽ kiểm soát nếu client code truyền một góc vượt khỏi phạm vi từ 0 độ cho đến 359 độ.

double STDIO_sin(int angle)
{
	while (angle > 359)
	{
		angle -= 360;
	}
	while (angle < 0)
	{
		angle += 360;
	}
	return s_sin_value[angle];
}

Xây dựng các "hàm" khác như cos, tang, cotang

Việc xây dựng các "hàm" khác để "tính" cos, tang, cotang sẽ tương tự như tính sin. Tuy nhiên, cần lưu ý rằng tang và cotang có điểm vô cực, do đó, cần kiểm soát được 2 giá trị vô cực ở các "điểm nóng" này. Với kinh nghiệm của tôi, nếu tại điểm vô cực của tang khi góc đo là 90 độ hoặc 270 độ thì tôi sẽ gán 2 giá trị là giá trị lớn nhất mà giới hạn của kiểu dữ liệu cho phép, do kiểu tôi đang chọn là double nên giá trị sẽ là ±1.7e±308 (~15 ký số). Tương tự cho 2 điểm cực đại vươn tới vô cực của cotang khi ở góc 0 độ và 180 độ.

Mẹo xây dựng nhanh các mảng dữ liệu

Cách 1

Để tạo ra được một mảng 360 phần tử, sẽ tốn rất nhiều công sức nếu ta sử dụng công cụ sau

ss_3

Cách 2

Thật tế tôi không dùng máy tính để tạo ra mảng trên, tôi viết chương trình để sinh ra code của mảng trên và có thể copy code đó vào chương trình chính. Giai đoạn đó tôi sử dụng Perl, đôi lúc dùng C# hoặc chính C++. Đoạn mã bằng C# bên dưới tôi dùng để sinh ra mảng các phần tử của sin.

using System;

namespace STDIO_CodeGenerator
{
    class Program
    {
        static void Main(string[] args)
        {
            const double PI = 3.14159265359;

            string result = "static double s_sin_value[360] = {";
            
            for (int i = 0; i < 360; i++)
            {
                
                result += Math.Sin(((double)i)*PI/180.0).ToString() + ",";
            }

            result += "};";

            System.IO.File.WriteAllText(@"D:\STDIO_math.h", result);

        }
    }
}

Việc TÍNH TRƯỚC này không chỉ có thể hiểu là mọi thứ đã diễn ra trước quá trình biên dịch mà các bạn có thể KHỞI TẠO các giá trị cần thiết cho các hàm trong quá trình NẠP của ứng dụng trước khi các bước thực thi chính diễn ra, ví dụ quá trình sinh ra mảng giá trị cho sin sẽ xảy ra trong quá trình nạp dữ liệu cho game (state loading).

Thuận lợi & Bất lợi

Thuận lợi

  • Việc TÍNH TRƯỚC kết quả trước thời điểm thực thi giúp cho chương trình ta thật tế đã có sẵn kết quả, chỉ việc lấy kết quả sử dụng do đó không tốn nhiều thời gian tính lại, giúp tối ưu hơn về tốc độ.
  • Giúp cho kết quả đồng nhất trên nhiều hệ thống tính toán khác nhau, có thể kết quả tính toán tùy vào độ chính xác mà các hệ thống khác nhau có thể khác nhau đôi chút, nhưng với việc tính toán trước từ một nguyên tắc chung thì kết quả sẽ đồng nhất.

Bất lợi

  • Tốn tài nguyên lưu trữ, nếu việc tính toán trước là hiệu ứng, hình ảnh (như particle trong game thay bằng Sprite) sẽ tốn nhiều vùng nhớ hơn.
  • Giảm độ linh động, ví dụ là particle trong game sẽ giảm đi độ ngẫu nhiên lúc thực thi (do hiệu ứng lưu trong Sprite đã cố định bằng các frame vẽ), về hàm tính sin như trên, ta cũng không thể làm được nhiều hơn như làm thêm cho các góc bất kỳ (kiểu số thực) vì sức ta sẽ có giới hạn.

Tùy vào nhiều điều kiện mà ta có quyết định chọn việc TÍNH TRƯỚC hay không? Trong trường hợp trên tôi làm game trên K800 là một máy khá yếu với thời điểm đó (2009) dành cho hàm lượng giác, điều đó có nghĩa là tôi phải làm tỉ mỉ hơn và nhiều việc hơn vì phải tự xây dựng lại các hàm lượng giác. Nhưng với các thiết bị thông minh như thời điểm này (2014), thì điều đó không còn là vấn đề lớn nữa, mà vấn đề lại liên quan tới những thứ khác nặng nề hơn như xử lý đồ họa, âm thanh, các giải thuật tìm đường, AI.

Lời kết

Việc TÍNH TRƯỚC không chỉ áp dụng cho trường hợp cụ thể này, mà các bạn có thể sử dụng tư tưởng này để tối ưu rất nhiều thành phần như đồ họa, xử lý font. Mặc dù hiện tại tôi ít áp dụng giải pháp tính toán trước lượng giác, nhưng không phải là lúc nào cũng không dùng, đôi lúc để điều khiển cho các vi xử lý nhỏ ở tầng dưới, thì đây là cách khá hữu hiệu để tăng khả năng tính toán (nhưng phải cân bằng với bộ nhớ của nó).

THẢO LUẬN
ĐÓNG