사인(sine) 함수를 구현하는데 가장 중심이 되는 전략은, 사인 함수와 같은 삼각 함수가 주기 함수라는 것이다. 즉, 입력값이 계속 변하는 것이 아니라, 일정값을 중심으로 계속해서 변하기 때문에 특정 입력 범위에 대해서 상수 시간의, 적절한 오차의 계산만 해주면 쓸만한 구현을 얻을 수 있다. 특히, 사인 함수를 테일러 급수(Taylor Series)를 전개해서 근사식을 얻어보면, 각 항의 분모가 매우 빠르게 수렴한다는 것을 알 수 있다.
먼저, 사인 함수를 테일러 급수를 사용해서 전개하면 다음과 같다.
sin(x) = x - (x^3 / 3!) + (x^5 / 5!) -(x^7 / 7!) + ....
즉, 테일러 급수로 전개했을 때 sin(x)의 일반항은,
sin(n)(x) = (-1)^n * ((x^(2n + 1) / (2n + 1)!)
이것저것 생각하지 않고 사인 함수를 직접 구현하고자 한다면, 테일러 급수를 사용해서 적절한 항까지 구현하고, 그냥 여기에 넣고 구현하면 된다. 그러나, 오차와 수행 속도를 생각하면 결과는 신통치 않을 것이다. 왜냐하면, 각 항을 계산하는데 팩토리얼 함수는 일반적으로 수행 성능이 시원치 않으며, 지수 함수를 계산하는 것도 만만치 않은 수행 시간을 소모하기 때문이다.
단순히 팩토리얼 값을 계산하는 것을 피하고 싶다면, sin(x)의 테일러 급수 전개항의 일반항에서, n 항과 n + 1 항을 비교해보면 n 항과 비교해서 어느 정도 더 커졌는지 알 수 있으므로, 팩토리얼 계산을 피할 수 있다. 그러나 이것은 앞으로 구현할 완전한 사인 함수의 계산에서 사용할 수 있는 트릭(Trick)은 되겠지만, 완전한 해결책이 아니다.
즉, 입력값이 매우 크다면, 여기에 대해 아무런 필터링 과정이 없기 때문에 5 ~ 6항 정도로는 적절한 값에 수렴하지 않을 확률이 매우 크다. 이 점에 착안해서 구현을 고쳐보자.
먼저, 입력값이 작을 때에는, 많은 항이 아니더라도 매우 빠르게 실제 값에 적절하게 근사하는 것을 볼 수 있다. 바로 이것을, 삼각 함수는 주기성을 가진다는 것에 착안하여 일정 범위 이상의 값은 재조정할 수 있다.
sin(π - x) = sin(x)
sin(π + x) = -sin(x)
sin(π / 2 - x) = cos(x)
sin(π / 2 + x) = -cos(x)
먼저, 입력값은 x mod 2π의 형태로 다시 쓸 수 있으며, 이렇게 얻은 값에 대해서 그 값이 어느 범위에 있는지를 판단해서 적절한 범위의 사인 함수를 다시 호출할 수 있다. 이것으로도, 값의 범위는 매우 줄어들었으며, 테일러 급수로 전개한 근사식의 일정한 갯수만 사용하더라도 쓸만한 오차 범위의 값을 구할 수 있다. 코사인(cosine) 함수도 이와 비슷한 방법으로 구현할 것이므로, 값의 범위에 따라 해당 삼각 함수를 호출하는 것은 큰 문제가 되지 않는다.
즉, 입력값이 작을수록, 적정 오차에 대해 필요한 테일러 급수로 전개한 근사식의 항의 갯수가 줄어든다는 것에 주목할 필요가 있다. 특히, 위의 식은 최소한 π /4 값에 대해서 값을 계산하도록 되어 있는데, 이 정도 범위라면 5 ~6항까지 계산하더라도 오차가 만만치 않을 것이다. 따라서, 여기서 한가지 더 최적화 방법으로 삼각함수의 덧셈 공식을 이용하도록 하자.
sin(A + B) = sin(A) * cos(B) + cos(A) * sin(B)
즉, π / 4 값에 대해서는 π / 6 + π / 12로 표현할 수 있으며, π / 12(15도)는 적은 항을 사용하더라도 적당한 오차 범위 이내로 계산할 수 있다. 그리고, sin(π / 6)의 값의 경우, 적절한 정확도로 미리 계산할 수 있기 때문에, 상수값으로 결정해둘 수 있다. 즉, 이것보다 더 적은 근사항으로 적절한 값을 얻고 싶다면, 어느 정도의 항이 필요한지 입력값의 크기, 항의 갯수, 오차의 범위를 측정해서 적용하면 될 것이다. 그리고, 실제로 사인값의 계산이 필요한 부분은, 위에서 언급한 팩토리얼 함수를 호출하지 않는 방법을 사용하거나, 아니면 미리 적정 범위로 정해진 테일러 급수로 전개한 근사식을 정리해서(Horner's Method와 같은) 계산하기 편한 형태로 만들어두면 될 것이다.
물론, 더 작은 각의 범위로 쪼갠다면 그만큼 코드가 길어지겠지만, 적절한 오차 이내의 값을 얻기 위해서 사용되는 계산 과정의 항을 하나라도 더 줄일 수 있을 것이다. 수학 함수는 그만큼 많이 호출되는 기본 함수이며, 그렇기에 이런 수고는 충분히 당위성을 가진다고 할 수 있다.
그리고, 코사인 함수는 사인 함수와 사실 위상(Phase) 차이 외에는 같기 때문에, 위의 전략을 동일하게 적용하여 구현할 수 있다. 그렇다면 탄젠트(tangent) 함수는? sin(x) / cos(x)이므로, 사인 함수와 코사인 함수를 구현했다면 간단히 이 값을 나눠서 구현할 수 있다.
Reference
Jack W. Crenshaw, Math Toolkit for Real-Time Programming, CMP Books
먼저, 사인 함수를 테일러 급수를 사용해서 전개하면 다음과 같다.
sin(x) = x - (x^3 / 3!) + (x^5 / 5!) -(x^7 / 7!) + ....
즉, 테일러 급수로 전개했을 때 sin(x)의 일반항은,
sin(n)(x) = (-1)^n * ((x^(2n + 1) / (2n + 1)!)
이것저것 생각하지 않고 사인 함수를 직접 구현하고자 한다면, 테일러 급수를 사용해서 적절한 항까지 구현하고, 그냥 여기에 넣고 구현하면 된다. 그러나, 오차와 수행 속도를 생각하면 결과는 신통치 않을 것이다. 왜냐하면, 각 항을 계산하는데 팩토리얼 함수는 일반적으로 수행 성능이 시원치 않으며, 지수 함수를 계산하는 것도 만만치 않은 수행 시간을 소모하기 때문이다.
단순히 팩토리얼 값을 계산하는 것을 피하고 싶다면, sin(x)의 테일러 급수 전개항의 일반항에서, n 항과 n + 1 항을 비교해보면 n 항과 비교해서 어느 정도 더 커졌는지 알 수 있으므로, 팩토리얼 계산을 피할 수 있다. 그러나 이것은 앞으로 구현할 완전한 사인 함수의 계산에서 사용할 수 있는 트릭(Trick)은 되겠지만, 완전한 해결책이 아니다.
즉, 입력값이 매우 크다면, 여기에 대해 아무런 필터링 과정이 없기 때문에 5 ~ 6항 정도로는 적절한 값에 수렴하지 않을 확률이 매우 크다. 이 점에 착안해서 구현을 고쳐보자.
먼저, 입력값이 작을 때에는, 많은 항이 아니더라도 매우 빠르게 실제 값에 적절하게 근사하는 것을 볼 수 있다. 바로 이것을, 삼각 함수는 주기성을 가진다는 것에 착안하여 일정 범위 이상의 값은 재조정할 수 있다.
sin(π - x) = sin(x)
sin(π + x) = -sin(x)
sin(π / 2 - x) = cos(x)
sin(π / 2 + x) = -cos(x)
먼저, 입력값은 x mod 2π의 형태로 다시 쓸 수 있으며, 이렇게 얻은 값에 대해서 그 값이 어느 범위에 있는지를 판단해서 적절한 범위의 사인 함수를 다시 호출할 수 있다. 이것으로도, 값의 범위는 매우 줄어들었으며, 테일러 급수로 전개한 근사식의 일정한 갯수만 사용하더라도 쓸만한 오차 범위의 값을 구할 수 있다. 코사인(cosine) 함수도 이와 비슷한 방법으로 구현할 것이므로, 값의 범위에 따라 해당 삼각 함수를 호출하는 것은 큰 문제가 되지 않는다.
즉, 입력값이 작을수록, 적정 오차에 대해 필요한 테일러 급수로 전개한 근사식의 항의 갯수가 줄어든다는 것에 주목할 필요가 있다. 특히, 위의 식은 최소한 π /4 값에 대해서 값을 계산하도록 되어 있는데, 이 정도 범위라면 5 ~6항까지 계산하더라도 오차가 만만치 않을 것이다. 따라서, 여기서 한가지 더 최적화 방법으로 삼각함수의 덧셈 공식을 이용하도록 하자.
sin(A + B) = sin(A) * cos(B) + cos(A) * sin(B)
즉, π / 4 값에 대해서는 π / 6 + π / 12로 표현할 수 있으며, π / 12(15도)는 적은 항을 사용하더라도 적당한 오차 범위 이내로 계산할 수 있다. 그리고, sin(π / 6)의 값의 경우, 적절한 정확도로 미리 계산할 수 있기 때문에, 상수값으로 결정해둘 수 있다. 즉, 이것보다 더 적은 근사항으로 적절한 값을 얻고 싶다면, 어느 정도의 항이 필요한지 입력값의 크기, 항의 갯수, 오차의 범위를 측정해서 적용하면 될 것이다. 그리고, 실제로 사인값의 계산이 필요한 부분은, 위에서 언급한 팩토리얼 함수를 호출하지 않는 방법을 사용하거나, 아니면 미리 적정 범위로 정해진 테일러 급수로 전개한 근사식을 정리해서(Horner's Method와 같은) 계산하기 편한 형태로 만들어두면 될 것이다.
물론, 더 작은 각의 범위로 쪼갠다면 그만큼 코드가 길어지겠지만, 적절한 오차 이내의 값을 얻기 위해서 사용되는 계산 과정의 항을 하나라도 더 줄일 수 있을 것이다. 수학 함수는 그만큼 많이 호출되는 기본 함수이며, 그렇기에 이런 수고는 충분히 당위성을 가진다고 할 수 있다.
그리고, 코사인 함수는 사인 함수와 사실 위상(Phase) 차이 외에는 같기 때문에, 위의 전략을 동일하게 적용하여 구현할 수 있다. 그렇다면 탄젠트(tangent) 함수는? sin(x) / cos(x)이므로, 사인 함수와 코사인 함수를 구현했다면 간단히 이 값을 나눠서 구현할 수 있다.
Reference
Jack W. Crenshaw, Math Toolkit for Real-Time Programming, CMP Books