상세 컨텐츠

본문 제목

BigFloat로 Pi를 구해보자-12. 코사인 함수를 이용한 계산법

프로그래밍/Multi precision

by ∫2tdt=t²+c 2012. 11. 26. 00:19

본문


저번 연재까지는 아크탄젠트 급수와, 이를 이용한 Machin 급수로 Pi를 계산하는 방법에 대해서 다뤘습니다. 이번에는 저번 방법보다 훨씬 빠른 코사인 함수를 이용한 계산법에 대해서 소개합니다.



위 점화식은 n이 무한히 커짐에 따라 Pi/2에 수렴합니다. x 값의 정밀도가 어느 정도인지에 상관없이 항상 그보다 더 정밀한 근사값을 계산해줍니다. 

오차가 얼마나 빨리 줄어드는지 계산해 볼까요?


오차를 입실론이라고 하면 다음과 같이 식을 세울 수 있습니다.


함수 꼴로 바꿔보면 조금더 보기 쉽겠네요


테일러 급수를 이용하면 다음 사실이 자명함을 알 수 있습니다.



그러므로 역시 다음도 성립합니다.


즉 오차는 매 반복마다 세제곱 한것보다 더 줄어듭니다. 다르게 말하면, 매 반복마다 정밀도가 최소 3배씩은 증가한다는 말입니다.

수렴속도도 빠를 뿐만 아니라, incremental 하다는 장점도 있습니다. (나눗셈 구현할때 사용했던 Newton-Raphson 방법처럼ㅋ : 7. 나눗셈 최적화와 큰O 표기법(Big-O Notation))

즉 처음에는 작은 정밀도로 계산해나가다가, 나중에 차차 정밀도를 키워가는 테크닉을 사용할 수 있다는 것이지요.




이 방법을 구현하기 위해서는 먼저 코사인 함수를 구현해야 합니다. 코사인 함수의 테일러 전개는 널리 잘 알려져 있지요.

분모가 팩토리얼로 늘어나면서 x값이 무엇이든간에 상관하지 않고 다 수렴시켜버립니다!! (오오미 팩토리얼 증가의 위대함!)

하지만 이 전개가 모든 값에 다 수렴한다고 해도, 빠른 수렴속도를 위해서는 최대한 작은 x를 고를 필요가 있습니다. 그래야 수렴속도가 빨라지니까요. 우리가 x로 사용할 값은 Pi/2 근처값이니 1보다 큽니다. 1보다 작으면 수렴 속도가 빨라서 참 좋을텐데요...


일단 코사인 함수를 구현해봅시다!

매번 x^2을 계산하는것은 시간/에너지 낭비니깐, 먼저 x^2을 계산해서 저장해두면 좋겠네요.

그리고 처음 계산한 x^2/2!는 다음에 x^4/4!를 계산할때 이용하면 좋겠네요. x^2/2! 에다가 x^2을 곱하고, 3*4를 나누면 바로 다음 항을 구할 수 있으니까요. 이런 테크닉을 이용해 3번의 곱셈과 1번의 덧셈을 이용해 하나의 항을 계산할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void RR_cos(PRR dest, PRR a)
{
//dest = cos(a)
    PRR a2 = RR_create(dest->size + 1);
    PRR t = RR_create(dest->size + 1);
    PRR u = RR_create(dest->size + 1);
    PRR rcp = RR_create(dest->size + 1);
 
    RR_initu(dest, 1, 0); //dest = 1
    RR_initu(t, 1, 0); //t = 1
 
    RR_mul(a2, a, a); //a2 = a^2
 
    for(RRU32 i = 1; i < 0xFFFFFFFE; i+=2)
    {
        RR_invu32(rcp, i*(i+1));
        RR_mul(u, t, a2);
        RR_mul(t, u, rcp);
//t = t* a^2 / (i*(i+1))
        if(RR_iszero(t))break;
//t가 0이 되면 탈출
        RR_neg(t);
        RR_add(dest, t);
    }
 
    RR_delete(rcp);
    RR_delete(t);
    RR_delete(u);
    RR_delete(a2);
}


불행히도 우리는 저 반복문이 몇번이나 돌지를 사전에 예측할 수가 없습니다 ㅠ(O(n)보다는 작지만, O(log n)보다는 큰 그 사이 어딘가에...). 확실한 건 초기값 a가 작아서, 항이 빠르게 감소하길 기대하는 것 뿐이지요. 곱셈이 시간복잡도가 O(n2)이었으니 코사인함수 전체의 시간복잡도는 O(n2log n)보다는 크고 O(n3)보다는 작게 되겠습니다.


하지만 저 속도에는 만족못하겠네요. 삼각함수의 반각공식이 있습니다.

cos x 를 계산하는 대신에, cos x/2를 계산해서 이것으로 cos x를 구하는 겁니다. 이 방법을 재귀적으로 사용하면 빠른 속도로 cos 을 계산해 낼수 있을 겁니다. x를 너무 많이 반으로 쪼개어 계산하면 오차가 누적되어 다른 값이 나올 수 있으니 주의해야합니다.

이 방법을 이용한 cos2함수입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void RR_cos2(PRR dest, PRR a)
{
    PRR ah = RR_create(dest->size + 1);
    PRR dh = RR_create(dest->size + 2);
    PRR eh = RR_create(dest->size + 2);
//오차 방지를 위해 넉넉히 공간을 잡는중
 
    RR_shrcopy(ah, a, 32);
//2로 32번 나눔
    RR_cos(dh, ah);
//위에서 구현한 cos함수 호출
     
    for(int i = 0; i < 31; ++i)
    {
        RR_mul(eh, dh, dh);
//제곱하고
        RR_shl(eh, 1);
//2를 곱하고
        if(i_subu32(eh->frac, eh->size, 1, 0))
//1을 빼준다
        {
            eh->sign = ! eh->sign;
            i_cmplt(eh->frac, eh->size);
//부호가 바뀌었을 경우 처리
        }
        PRR t = eh;
        eh = dh;
        dh = t;
    }
 
    RR_mul(dest, dh, dh);
    RR_shl(dest, 1);
    if(i_subu32(dest->frac, dest->size, 1, 0))
    {
        dest->sign = ! dest->sign;
        i_cmplt(dest->frac, dest->size);
    }
 
    RR_delete(eh);
    RR_delete(dh);
    RR_delete(ah);
}


RR_cos2함수는 RR_cos함수보다 훨씬 빠릅니다. 문제는 약간 오차가 발생한다는 건데, 그 정도는 충분히 감수할 만하지요. 오차문제는 dest의 size를 처음에 넉넉하게 잡음으로써 해결할 수 있습니다.


자 이제 우리의 최종 목표였던 Pi를 계산해봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void RR_pi_cos(PRR dest)
{
//RR_cos 함수를 사용하는 버전
    PRR t = RR_create(dest->size);
    RR_init(dest, 1.57);
 
    t->size = 3;
    dest->size = 3;
    RRU32 c = 123456789;
    for(int i = 0;; ++i)
    {
        RR_cos(t, dest);
        RR_add(dest, t);
//dest += cos(dest)
        if(t->size == t->alloced)
        {
            if(c == dest->frac[dest->size - 2])break;
//수치가 안정화되면 탈출
            c = dest->frac[dest->size - 2];
        }
        t->size = MIN(t->size*3, t->alloced);
//정밀도를 3배씩 증가시킵니다.
        dest->size = t->size;
    }
 
    RR_shl(dest, 1);
//결과물 2배해주기
    RR_delete(t);
}
 
void RR_pi_cos2(PRR dest)
{
//RR_cos2를 사용하는 버전
    PRR t = RR_create(dest->size);
    RR_init(dest, 1.57);
 
    t->size = 3;
    dest->size = 3;
    RRU32 c = 123456789;
    for(int i = 0;; ++i)
    {
        RR_cos2(t, dest);
        RR_add(dest, t);
//dest += cos(dest)
        if(t->size == t->alloced)
        {
            if(c == dest->frac[dest->size - 2])break;
//수치가 안정화되면 탈출
            c = dest->frac[dest->size - 2];
        }
        t->size = MIN(t->size*3, t->alloced);
//정밀도를 3배씩 증가시킵니다.
        dest->size = t->size;
    }
 
    RR_shl(dest, 1);
//결과물 2배해주기
    RR_delete(t);
}


32비트 512 길이 (즉 32*512비트 정밀도)

RR_pi_cos2 함수의 압도적인 승리입니다!

관련글 더보기

댓글 영역