파이썬은 C/C++ 과 같은 컴파일 기반 언어에 비해 느립니다. 하지만 프로파일링으로 프로그램 속도의 병목 지점을 찾아 최소한의 노력으로도 성능을 최대한 끌어올릴 수 있습니다. 파이썬에서는 timeit 같은 매직 명령어와 cProfile, line_profiler, memory profiler 등의 여러 프로파일 패키지 및 함수를 제공하며, 이를 통해 병목이 발생하는 함수, 함수 안의 라인까지도 소요 시간/메모리 등을 자세하게 측정할 수 있고 CPU 뿐만 아니라 GPU, 네트워크 대역폭, 디스크 I/O 등 사용되는 거의 모든 리소스에 대해서 측정할 수 있습니다.
줄리아 집합 (Julia set)
줄리아 집합은 가스통 줄리아가 고안한 프랙탈의 일종으로 주어진 복소수 $c$에 대해서 다음 점화식에 따라 정의된 수열이 발산하지 않는 복소수 $z$의 집합으로 정의됩니다.
$z_{n+1} = z_n^2 + c$
예를 들어 주어진 $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
'Computer > Python' 카테고리의 다른 글
프로파일링 (3) - cProfile 모듈 (0) | 2022.08.20 |
---|---|
프로파일링 (2) - 함수 실행 시간 계산하기 (0) | 2022.08.13 |
최소 지식의 원칙과 클래스 메소드 (0) | 2022.07.17 |
파이썬 dataclasses 표준 라이브러리 (3) | 2022.03.31 |
정규표현식을 이용해 문자열에서 숫자 찾기 (0) | 2022.03.25 |