고등학생 때 수학을 공부하다 보면 은근히 적분에서 막히는 때가 많습니다. 미분은 쉬운데 이상하게 적분은 어렵죠. 아마도 미분의 역순이라는, 거꾸로 무엇인가 해야 한다는 점이 어려운 것 같습니다. 적분이라는 개념은 과학 분야에서 종종 사용되는데, 프로그래밍 언어로 구현하는 것이 생각보다 까다로운 것 같아 블로그에 정리해봤습니다.
먼저 C언어에서 적분 함수를 만드는 방법입니다. C언어에서 다항함수를 적분하는 함수를 작성하는 방법은 다음과 같습니다.
1. 함수 프로토타입 정의
먼저, 함수의 프로토타입을 선언해야 합니다. 이는 함수가 받아들이는 인수와 반환하는 값의 자료형을 미리 명시해주는 역할을 합니다. 적분 함수의 경우, 다음과 같은 프로토타입을 갖습니다.
double integrate(double *coefficients, int n, double a, double b);
이 함수는 double형 배열 coefficients, 배열의 길이 n, 구간 [a,b]를 입력으로 받아 적분한 결과를 double형으로 반환합니다.
2. 함수 작성
적분 함수는 다음과 같은 단계를 거치며 적분값을 계산합니다.
- 적분을 계산할 구간 [a,b]를 분할하여 직사각형으로 근사합니다.
- 각 직사각형의 넓이를 계산하여 더합니다.
- 결과를 반환합니다.
double integrate(double *coefficients, int n, double a, double b) { double sum = 0.0; double dx = (b - a) / N; // N은 적분할 구간을 나누는 수 for (int i = 0; i < N; i++) { double x0 = a + i * dx; double x1 = a + (i + 1) * dx; double y0 = 0.0; double y1 = 0.0; for (int j = n - 1; j >= 0; j--) { y0 = y0 * (x0 - a) + coefficients[j]; y1 = y1 * (x1 - a) + coefficients[j]; } double area = (y0 + y1) * dx / 2.0; sum += area; } return sum; }
위 코드에서 coefficients는 다항식의 계수를 저장한 배열이며, n은 다항식의 차수입니다. dx는 구간을 나누는 수로, 구간을 더 작은 구간으로 나누어 적분을 수행합니다. 이 코드에서는 구간을 N으로 나누도록 되어있습니다.
다항식을 계산하는 부분에서는 x0와 x1은 직사각형의 왼쪽과 오른쪽 끝점입니다. 그리고 y0와 y1은 다항식을 x0와 x1에서 계산한 결과입니다. 다항식을 계산할 때는 다항식의 계수를 이용함으로써 Horner 방법을 사용하였습니다.
3. 함수 호출
이제 작성한 integrate 함수를 호출하여 적분값을 계산할 수 있습니다. 예를 들어, 1 + 2x + 3x^2 다항식의 [0,1] 구간에서의 적분값을 계산하려면 다음과 같이 호출합니다.
int main() { double coefficients[] = {1.0, 2.0, 3.0}; int n = 3; double a = 0.0; double b = 1.0; double result = integrate(coefficients, n, a, b); printf("The integral of 1 + 2x + 3x^2 from 0 to 1 is: %f", result); return 0; }
위 코드에서 coefficients는 다항식의 계수를 저장한 배열이며, n은 다항식의 차수입니다. a와 b는 각각 적분 구간의 시작점과 끝점입니다. integrate 함수를 호출하여 적분 값을 계산한 후, 결과를 출력합니다.
위 코드를 실행하면 “The integral of 1 + 2x + 3x^2 from 0 to 1 is: 2.333333″이 출력될 것입니다.
다음으로는 Java에서 적분 함수를 작성하는 방법에 대해서 알아보겠습니다.
4. Java에서 적분 함수 만들기
C언어에서 작성했던 적분 함수와 같습니다. Java에서 사용할 수 있도록 약간 수정한 것에 불과하여서 간단하게만 정리하도록 하겠습니다.
다음은 Java에서 적분 함수를 구현하는 방법입니다.
public static double integrate(double[] coefficients, double a, double b) { int N = 1000; // 구간을 1000 등분하여 적분합니다. double dx = (b - a) / N; double sum = 0.0; for (int i = 0; i < N; i++) { double x0 = a + i * dx; double x1 = a + (i + 1) * dx; double y0 = 0.0; double y1 = 0.0; for (int j = coefficients.length - 1; j >= 0; j--) { y0 = y0 * (x0 - a) + coefficients[j]; y1 = y1 * (x1 - a) + coefficients[j]; } double area = (y0 + y1) * dx / 2.0; sum += area; } return sum; }
위 코드에서 coefficients는 다항식의 계수를 저장한 배열이며, a와 b는 적분 구간의 시작점과 끝점입니다. N은 구간을 나누는 수로, 이 예제에서는 1000으로 설정되어 있습니다.
위에서 작성한 integrate 함수를 호출하여 적분값을 계산할 수 있습니다. 예를 들어, 1 + 2x + 3x^2 다항식의 [0,1] 구간에서의 적분값을 계산하려면 다음과 같이 호출합니다.
public static void main(String[] args) { double[] coefficients = {1.0, 2.0, 3.0}; double a = 0.0; double b = 1.0; double result = integrate(coefficients, a, b); System.out.println("The integral of 1 + 2x + 3x^2 from 0 to 1 is: " + result); }
위 코드에서 coefficients는 다항식의 계수를 저장한 배열이며, a와 b는 각각 적분 구간의 시작점과 끝점입니다. integrate 함수를 호출하여 적분 값을 계산한 후, 결과를 출력합니다.
C언어에서 실행했던 결과와 같은 내용이 출력될 것입니다.
지금까지 잘 봐왔다면 파이썬으로 적분 함수를 만드는 방법도 C언어나 Java와 크게 다를 게 없을 것임을 알아차렸을 겁니다.
5. Python에서 적분 함수 만들기
아래는 파이썬에서 다항함수 적분을 구현하는 함수입니다.
def integrate(coefficients, a, b): N = 1000 # 구간을 1000 등분하여 적분합니다. dx = (b - a) / N sum = 0.0 for i in range(N): x0 = a + i * dx x1 = a + (i + 1) * dx y0 = 0.0 y1 = 0.0 for j in range(len(coefficients) - 1, -1, -1): y0 = y0 * (x0 - a) + coefficients[j] y1 = y1 * (x1 - a) + coefficients[j] area = (y0 + y1) * dx / 2.0 sum += area return sum
위 코드에서 coefficients는 다항식의 계수를 저장한 리스트이며, a와 b는 적분 구간의 시작점과 끝점입니다. N은 구간을 나누는 수로, 이 예제에서는 1000으로 설정되어 있습니다.
위에서 작성한 integrate 함수를 호출하여 적분 값을 계산할 수 있습니다. 예를 들어, 1 + 2x + 3x^2 다항식의 [0,1] 구간에서의 적분 값을 계산하려면 다음과 같이 호출합니다. (앞의 예제와 같습니다.)
coefficients = [1.0, 2.0, 3.0] a = 0.0 b = 1.0 result = integrate(coefficients, a, b) print("The integral of 1 + 2x + 3x^2 from 0 to 1 is:", result)
위 코드에서 coefficients는 다항식의 계수를 저장한 리스트이며, a와 b는 각각 적분 구간의 시작점과 끝점입니다. integrate 함수를 호출하여 적분값을 계산한 후, 결과를 출력합니다.
위 코드를 실행하면, “The integral of 1 + 2x + 3x^2 from 0 to 1 is: 2.33333333333″이 출력됩니다.
Python에서는 C언어와 달리 배열 대신 리스트를 사용합니다. 또한, Python에서는 range 함수를 사용하여 반복문을 작성할 수도 있습니다.
6. Javascript에서 적분 함수 만들기
자바스크립트에서는 사원근사법으로 적분 함수를 구현할 수 있습니다. 사원 근사법은 구간을 4개의 사원형 영역으로 나누어 각 영역의 면적을 근사하는 방법입니다. 이를 자바스크립트로 구현하기 위해서는 다음과 같은 코드를 작성할 수 있습니다.
function integrate(f, a, b, N) { const dx = (b - a) / N; let sum = 0; for (let i = 0; i < N; i++) { const x0 = a + i * dx; const x1 = a + (i + 1) * dx; const mid = (x0 + x1) / 2; sum += f(x0) * (7 + f(mid) * 32 + f(x1) * 12) / 45 * dx; sum += f(x1) * (7 + f(mid) * 32 + f(x0) * 12) / 45 * dx; } return sum; } function integratePoly(coeffs, a, b, N) { const poly = x => coeffs.reduce((sum, c, i) => sum + c * x ** i, 0); return integrate(poly, a, b, N); }
이 함수는 다음과 같은 인자를 받습니다.
- f: 적분할 함수
- a: 적분 구간의 하한
- b: 적분 구간의 상한
- N: 구간을 나눌 개수
이 함수는 구간을 N개의 사다리꼴 영역으로 분할하고, 각 영역을 사원형 영역 4개로 나눈 뒤, 각 영역의 면적을 근사하여 합산한 값을 반환합니다.
다음은 2차 함수 y = x^2를 x = 0부터 x = 1까지 적분하는 예시입니다.
const coeffs = [0, 0, 1]; // 2차항 계수 = 1 const a = 0; const b = 1; const N = 100; const result = integratePoly(coeffs, a, b, N); console.log(result); // 0.3333333333333333
사원 근사법은 사다리꼴 근사법보다 정확도가 높지만 계산 비용이 더 많이 들기 때문에 구간을 적절하게 나누는 것이 중요합니다. 따라서 N 값은 적절한 값을 찾아서 사용해야 합니다.
다음은 4차 함수 y = x^4 + x^3 + x^2 + x를 x = 0부터 x = 1까지 적분하는 예시입니다.
const coeffs = [0, 1, 1, 1, 1]; // 4차항 계수 = 1, 3차항 계수 = 1, 2차항 계수 = 1, 1차항 계수 = 1 const a = 0; const b = 1; const N = 1000; const result = integratePoly(coeffs, a, b, N); console.log(result); // 0.40833355000000006
integratePoly 함수를 이용하여 적분값을 계산한 후 그 결과를 출력합니다. 위 예시에서는 구간을 1,000개의 사다리꼴 영역으로 분할하여 계산하였습니다.
7. 합성함수를 적분하는 함수 만들기
합성함수의 적분을 구하는 함수를 C언어로 작성하는 방법은 다음과 같습니다.
double integrate(double a, double b, double (*func)(double), double (*g)(double)) { double c = g(a); double d = g(b); int N = 1000; // 구간을 1000 등분하여 적분합니다. double dx = (d - c) / N; double sum = 0.0; for (int i = 0; i < N; i++) { double x0 = c + i * dx; double x1 = c + (i + 1) * dx; double y0 = func(g(x0)); double y1 = func(g(x1)); double area = (y0 + y1) * dx / 2.0; sum += area; } return sum; }
위 코드에서 func는 적분할 함수를 가리키는 함수 포인터이며, g는 합성함수를 가리키는 함수 포인터입니다. dx는 구간을 나누는 수로, 구간을 더 작은 구간으로 나누어 적분을 수행합니다.
이제 작성한 integrate 함수를 호출하여 적분값을 계산할 수 있습니다. 예를 들어, e^x 함수의 [0,1] 구간에서의 적분값을 구하되, 합성함수를 g(x) = x^2으로 설정하려면 다음과 같이 호출합니다.
int main() { double a = 0.0; double b = 1.0; double (*func)(double) = exp; double (*g)(double) = square; double result = integrate(a, b, func, g); printf("The integral of e^x composed with x^2 from 0 to 1 is: %f", result); return 0; }
위 코드에서는 exp 함수와 square 함수를 각각 func과 g로 전달하여 integrate 함수를 호출하여합성함수를 이용하여 적분을 수행합니다. integrate 함수를 호출하여 적분 값을 계산한 후, 결과를 출력합니다.
위 코드를 실행하면 “The integral of e^x composed with x^2 from 0 to 1 is: 1.046451″이 출력될 것입니다.
8. x86 어셈블리어(…!)로 적분 함수 만들기
인터넷을 찾다 보니 호기심이 생겨서 어셈블리어로도 적분 함수를 만들 수 있는지 알아봤습니다. 너무 복잡해서 제가 전부 이해할 수는 없었고… 직접 실행을 해보지는 않았지만, 혹시 필요하시면… 실행해보시고 잘 되는지 피드백해주시면 감사하겠습니다. 😎
다음은 함수 프롤로그와 에필로그입니다. 함수 프롤로그는 함수가 호출될 때 스택 프레임을 생성하고, 함수 인자를 스택에 저장하는 등의 작업을 수행하는 부분입니다. 에필로그는 함수가 종료될 때 스택 프레임을 제거하는 등의 작업을 수행하는 부분입니다.
section .text global integrate_asm integrate_asm: push ebp mov ebp, esp sub esp, 16 ; 스택 프레임 생성
함수 인자는 스택 프레임에서 ebp-8부터 ebp+12까지의 위치에 저장됩니다. 이를 읽어서 각각의 레지스터에 저장합니다.
mov eax, [ebp+8] ; 적분 구간의 하한 mov ebx, [ebp+12] ; 적분 구간의 상한 mov ecx, [ebp+16] ; 다항식 계수를 가리키는 포인터
구간을 나누어 사다리꼴 근사를 계산합니다. 이때, ecx 레지스터에 저장된 포인터를 이용하여 다항식 계수를 읽어옵니다.
mov edx, 1000 ; 구간을 1000등분하여 적분합니다. fld qword [ebp+8] ; x0를 스택에 저장 fld qword [ebp+12] ; x1을 스택에 저장 fsubp ; x1-x0을 스택에 저장 fild dword [edx] ; N을 스택에 저장 fdivrp ; (x1-x0)/N을 스택에 저장 fxch st1 fstp st0 fld1 ; 1을 스택에 저장 fldz ; 0을 스택에 저장 fxch st1 .loop: ; x0와 x1 사이에서 다항식 계산 fld qword [esp+4] ; x0을 스택에 저장 fld1 ; 1을 스택에 저장 fldz ; 0을 스택에 저장 fldz ; 0을 스택에 저장 fldz ; 0을 스택에 저장 fld qword [ecx+24] ; 4차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx+16] ; 3차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx+8] ; 2차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx] ; 1차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [esp+4] ; x0을 스택에 저장 fmul st0, st5 fxch st1 faddp st4, st0 ; y0 계산 완료 fxch st1 fld qword [esp+4] ; x0을 스택에 저장 fmul st0, st5 fxch st1 fmul st0, st5 fxch st1 fmul st0, st5 fxch st1 fld qword [ecx+32] ; 상수항 계수를 스택에 저장 faddp st4, st0 ; y0 계산 완료 fxch st1 ; x0와 x1 사이에서 사다리꼴 근사 계산 mov eax, 1 .loop_inner: cmp eax, edx jge .done fld qword [esp+4] ; x0을 스택에 저장 fild dword [eax] ; i를 스택에 저장 fmul st0, st4 ; h*i+x0를 스택에 저장 fxch st1 fld qword [esp+4] ; x0을 스택에 저장 faddp st4, st0 ; x0+i*h를 스택에 저장 fxch st1 fld qword [ecx+24] ; 4차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx+16] ; 3차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx+8] ; 2차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx] ; 1차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fmul st0, st4 ; 다항식 계산 fxch st1 fld qword [esp+4] ; x0을 스택에 저장 faddp st4, st0 ; x0+(i+1)*h를 스택에 저장 fxch st1 fld qword [ecx+24] ; 4차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx+16] ; 3차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx+8] ; 2차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fld qword [ecx] ; 1차항 계수를 스택에 저장 fmul st0, st5 fxch st1 fmul st0, st4 ; 다항식 계산 fxch st1 faddp st3, st0 ; y와 더함 fxch st1 inc eax ; i를 1 증가시킴 jmp .loop_inner .done: ; 사다리꼴 근사값을 반환 fmul st0, st2 fld1 fdivrp mov esp, ebp pop ebp ret
어셈블리어는 기계어에 가깝기 때문에 이해하는 것이 쉽지 않은 것 같습니다. 실력 좋은 프로그래머가 되려면 어셈블리어를 잘 알아야 한다는데… 잘 모르겠습니다.