본문 바로가기

Computer/Python

프로파일링 (1) - 줄리아 집합 (Julia set)

반응형

파이썬은 C/C++ 과 같은 컴파일 기반 언어에 비해 느립니다. 하지만 프로파일링으로 프로그램 속도의 병목 지점을 찾아 최소한의 노력으로도 성능을 최대한 끌어올릴 수 있습니다. 파이썬에서는 timeit 같은 매직 명령어와 cProfile, line_profiler, memory profiler 등의 여러 프로파일 패키지 및 함수를 제공하며, 이를 통해 병목이 발생하는 함수, 함수 안의 라인까지도 소요 시간/메모리 등을 자세하게 측정할 수 있고 CPU 뿐만 아니라 GPU, 네트워크 대역폭, 디스크 I/O 등 사용되는 거의 모든 리소스에 대해서 측정할 수 있습니다.

 

줄리아 집합 (Julia set)

줄리아 집합은 가스통 줄리아가 고안한 프랙탈의 일종으로 주어진 복소수 $c$에 대해서 다음 점화식에 따라 정의된 수열이 발산하지 않는 복소수 $z$의 집합으로 정의됩니다.

$z_{n+1} = z_n^2 + c$

 

 

쥘리아 집합 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

 

예를 들어 주어진 $c$에 대해 $z^2+c$ 가 2가 넘으면 발산한다고 했을 때, $c$값에 따라 발산하지 않는 $z$ 집합 (줄리아 집합) 이 다르게 형성될 것이고 이를 $z$ 좌표계를 그림으로 표현하면 반복횟수가 적을수록 어두운 색을 띠고 반복이 거듭될수록 밝은 색을 띠게 됩니다. 따라서 하얀색 영역은 계산이 더 복잡해서 생성되는 시간이 오래 걸리겠죠. 줄리아 집합 구현은 CPU를 매우 많이 사용하여 CPU와 RAM 사용량을 프로파일해서 코드의 어느 부분이 CPU나 RAM을 많이 쓰는지 확인하기 좋습니다.

 

Psuedo code

iter_per_z  # coordinate (z 좌표 별 반복 횟수)
for z in coordinates:  # X/Y grid
    for iteration in range(max iter):
    	if abs(z) < 2.:
        	z = z*z + c
            iter_per_z += 1
       	else:
        	break
    plot(iter_per_z)

 

Examples

$c=-0.62772-0.42193j$에 대해서 $z=0+0j$라 할 때, 다음처럼 $z$값이 계속 2보다 작으므로 루프를 계속 진행할 겁니다. 심지어 무한 루프일 수도 있습니다. 따라서 최대 반복 횟수를 지정해서 무한 루프를 방지해야 하며, 각 $z$ 별로 요구되는 반복횟수가 다르고 독립적으로 계산되어 데이터를 공유하지 않으므로 추후 완전히 병렬로 계산할 수 있습니다.

c = -0.62772-0.42193j
z = 0+0j
for n in range(10):
    z = z*z + c
    print(f'{n}: z={z:.5f}, abs(z)={abs(z):.3f}')
    
>>>
0: z=-0.62772-0.42193j, abs(z)=0.756
1: z=-0.41171+0.10778j, abs(z)=0.426
2: z=-0.46983-0.51068j, abs(z)=0.694
3: z=-0.66777+0.05793j, abs(z)=0.670
4: z=-0.18516-0.49930j, abs(z)=0.533
5: z=-0.84274-0.23703j, abs(z)=0.875
6: z=0.02630-0.02242j, abs(z)=0.035
7: z=-0.62753-0.42311j, abs(z)=0.757
8: z=-0.41295+0.10910j, abs(z)=0.427
9: z=-0.46910-0.51203j, abs(z)=0.694

 

Implementation

numpy 등의 라이브러리 없이 순수 리스트와 파이썬 문법만으로 구현해 보겠습니다. 먼저, $z$ 좌표계와 $c$가 인자로 들어왔을 때, 루프를 계산하는 함수입니다. ($c$는 고정되었다고 가정합니다)

def calculate_z(maxiter, zs, cs):
    output = [0] * len(zs)
    for i in range(len(zs)):
        n = 0
        z = zs[i]
        c = cs[i]
        while abs(z) < 2 and n < maxiter:
            z = z*z + c
            n += 1
        output[i] = n
    return output

 

이제 전체 함수를 구현합니다. 대략적인 함수 수행 시간을 측정하기 위해 time 모듈을 사용합니다.

import time

x1, x2, y1, y2 = -1.8, 1.8, -1.8, 1.8
c_real, c_img = -0.62772, -0.42193
def calculate_julia(desired_width=1000, maxiter=300):
    x_step = (x2 - x1) / desired_width
    y_step = (y1 - y2) / desired_width

    x = []
    x_coord = x1
    while x_coord < x2:
        x.append(x_coord)
        x_coord += x_step
    y = []
    y_coord = y2
    while y_coord > y1:
        y.append(y_coord)
        y_coord += y_step

    zs = []
    cs = []
    for ycoord in y:
        for xcoord in x:
            zs.append(complex(xcoord, ycoord))
            cs.append(complex(c_real, c_img))
    
    print(f"Total elements: {len(zs)}")
    start_time = time.time()
    output = calculate_z(maxiter, zs, cs)
    end_time = time.time()
    elapsed = end_time - start_time
    print(calculate_z.__name__, f" took {elapsed} seconds")
    return output
    
>>> calculate_julia()
Total elements: 1000000
calculate_z  took 10.16293454170227 seconds

 


프로파일링 (2) - 함수 실행 시간 계산하기

프로파일링 (3) - cProfile 모듈

프로파일링 (4) - line_profiler

프로파일링 (5) - 바이트코드: 내부작동 이해하기

반응형