반응형
[목차]
[1] Python : GIL 에 대하여
[2] Python 에서 멀티쓰레드 구현 가능?
[3]Thread 성능 비교
[4] Python의 CPU-bound 동작에서 Thread 병목 현상
[5] 사내 프로젝트 개선 결과

 

최근 프로젝트 유지보수를 진행하면서 에러는 아니지만 기능개선할 부분들이 눈에 들어왔다. 그중에서도 가장 눈에 띈 것은 기존 로직의 속도 개선이다.

속도개선하는 방법으로 먼저 멀티쓰레드에 대해 알아보았다. 하지만 프로젝트가 어떤 환경으로 구성되어있는지에 따라 적용할수 있는 방식이 다를수 있으므로 대상 프로젝트에 기반하여 작업 방법을 모색해보았다.

 

[프로젝트 스펙]

- 언어 : Python

- 프레임워크 : Flask

- DB : NoSQL (MongoDB)

- Server : EC2

 

[1] Python : GIL 에 대하여

python은 언어 그 자체에서 GIL 이 적용되어 있다. GIL은 Global Interpreter Lock의 약자로 쉽게 말해서 언어자체적으로 싱글 쓰레드만을 지원한다는 뜻이다.

GIL이 생겨나게 된 배경에는 Python의 GC 동작 방식이 Reference count 방식이기 때문이다.

 

> GC

Garbage collector의 약자로 사용하지 않는 메모리를 반환하는 방식이다. python에서는 Reference count를 확인하여 GC가 동작하는데 Java의 경우 객체가 "도달가능한지" 를 판별하여 도달불가능한 객체인 경우 메모리를 회수하는 방식으로 동작한다.

 

> Reference count

참조횟수 카운트 방식이다. python에서는 모든 것이 객체이고 각 객체는 이것이 참조되는 횟수에 대한 데이터를 저장해 놓는다. 참조가 될때마다 이 수는 차감되고 최종 값이 0이 되면 해당 객체에 대한 메모리를 반환한다.

 

> 그래서 python 은?

멀티 쓰레드 환경이 될 경우 공유자원에 대한 reference count 관리가 복잡해진다. 뮤텍스와 같은 다양한 방식으로 이를 처리해 주어야 하는데, 이런 복잡한 상황을 예방하고자 애초에 python에는 GIL이 적용되어 한개 인터프리터에서는 한개 쓰레드만이 바이트코드에서 동작하도록 설계된 것이다.

[2] Python에서 멀티쓰레드 구현 가능?

그렇다고 해서 아예 멀티쓰레드 구현이 안되는건 아니다. 위에서도 말했다싶이 python에도 Treading 라이브러리가 존재한다. GIL이 적용되는 것은 로직이 CPU-bound 일때 적용되는 것이도 파일의 읽고쓰기와 같이 I/O - bound 작업에서는 해당 작업중 발생하는 대기시간 동안 다른 쓰레드가 동작할 수 있다.

즉, 어떤 로직인지 그 유형에 따라 적절하게 멀티쓰레드를 구현하면 원하는 개선효율을 얻을 수 있다.

[3] Thread 성능 비교

(1) CPU - bound

먼저 단순히 for loop를 이용한 로직에서 멀티쓰레드를 적용했을때와, 적용하지 않았을때를 비교해 보았다.

def only_count(n):
    while n > 0:
        n -= 1

big_num = 100000000 # 1억
start = time.time()
t1 = Thread(target=only_count, kwargs={"n": big_num//2})
t2 = Thread(target=only_count, kwargs={"n": big_num//2})
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(f"[쓰레드 | 소요시간] {end - start} 초")
>>> [쓰레드 | 소요시간] 4.3975160121917725 초

start = time.time()
only_count(big_num)
end = time.time()
print(f"[일반 | 소요시간] {end - start} 초")
>>> [일반 | 소요시간] 4.546915769577026

1억의 숫자를 while 문을 이용해 반복했을때 그냥 호출했을때와 쓰레드 2개로 했을때 소요시간에 유의미한 차이가 나지 않는다. 오히려 멀티쓰레드로 동작했을때가 더 오래 걸리는 경우도 있었다.

(2) I/O - bound : time.sleep()

python의 time 라이브러리를 사용해 sleep을 걸게 되면 I/O bound 동작이므로 쓰레드 적용에 대한 효과를 볼수 있다.

> time.sleep()

time.sleep() 메서드는 특정 시간 동안 프로세스를 중지시키는 함수이다 이 함수를 호출하면 현재 스레드의 실행이 지정된 시간 동안 일시 중지되는데 이는 CPU 자원을 사용하지 않고, 단순히 대기하고 있기 때문에 I/O 바운드 작업으로 분류된다.

def countdown_sleep(n, t_num=None):
    while n > 0:
        if t_num:
            print(f"Thread {t_num} >>> {n}")
        else:
            print(f"일반 >>> {n}")
        n -= 1
        time.sleep(1)
sleep_num = 10
start = time.time()
t1 = Thread(target=countdown_sleep, kwargs={"n": sleep_num//2, "t_num": 1})
t2 = Thread(target=countdown_sleep, kwargs={"n": sleep_num//2, "t_num": 2})
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(f"[쓰레드 | 소요시간] {end - start} 초")
>>> [쓰레드 | 소요시간] 5.0233330726623535 초

start = time.time()
countdown_sleep(sleep_num)
end = time.time()
print(f"[일반 | 소요시간] {end - start} 초")
>>> [일반 | 소요시간] 10.040925979614258

 

sleep 을 주어 I/O 바운드 작업이 있는 로직에서는 두개의 쓰레드로 동작했을때 단일 쓰레드 보다 소요시간이 절반으로 줄어드는 것을 확인할 수 있었다.

한가지 동작을 두개로 나누어 진행시켰을때 시간도 절반이 될것! 이라는 기대가 충족되었다.

쓰레드가 동작하는 과정을 보기 위해 countdown_sleep 함수에서 print를 찍어보았다.

# 쓰레드 동작
Thread 1 >>> 5
Thread 2 >>> 5
Thread 1 >>> 4
Thread 2 >>> 4
Thread 2 >>> 3
Thread 1 >>> 3
Thread 2 >>> 2
Thread 1 >>> 2
Thread 2 >>> 1
Thread 1 >>> 1

# 일반 동작
일반 >>> 10
일반 >>> 9
일반 >>> 8
일반 >>> 7
일반 >>> 6
일반 >>> 5
일반 >>> 4
일반 >>> 3
일반 >>> 2
일반 >>> 1

두개의 쓰레드가 동시에 진행된 것을 볼 수 있다.

 

[4] Python의 CPU-bound 동작에서 Thread 병목 현상

CPU-bound 로직에서 단일쓰레드와 멀티쓰레드 동작을 비교해 보면 두개 처리시간이 같을때도 있지만 심지어 멀티쓰레드로 했을때 더 오래 걸리는 경우도 있다.

 

5000의 숫자를 단일쓰레드로 처리할때와 절반씩 나눠 멀티쓰레드로 처리했을때 소요시간을 측정해보았다.

def only_count(n):
    while n > 0:
        n -= 1

big_num = 5000
start = time.time()
t1 = Thread(target=only_count, kwargs={"n": big_num//2})
t2 = Thread(target=only_count, kwargs={"n": big_num//2})
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(f"[쓰레드 | 소요시간] {end - start} 초")
>>> [쓰레드 | 소요시간] 0.00043082237243652344 초

start = time.time()
only_count(big_num)
end = time.time()
print(f"[일반 | 소요시간] {end - start} 초")
>>> [일반 | 소요시간] 0.00023221969604492188

멀티쓰레드로 구현했을때 단일 쓰레드보다 약 2배의 시간이 소요된 것을 확인할수 있는데 이는 python 의 GIL 방식으로 인해 쓰레드간 경쟁이 일어나 오버헤드가 발생했기 때문이다. 따라서 CPU-bound 로직에서의 멀티쓰레드는 구현하지 않는게 좋다. 

 

> 오버헤드 & 콘텍스트 스위칭

멀티프로세스 에서 cpu가 실행중인 프로세스를 변경할때 콘텍스트 스위칭이 발생한다. 이때 레지스터에 저장된 메모리를 변경하는데 시간이 소요되면서 오버헤드가 발생한다고 한다. 멀티쓰레드의 경우 한개 프로세스 안에서 스택영역을 제외한 다른 메모리는 모두 공유하기 때문에 멀티 프로세스의 오버헤드보단 덜 하지만, 그럼에도 멀티쓰레드에서도 오버헤드가 발생한다.

 

[5] 사내 프로젝트 개선 결과

개선하려는 로직은 외부 api를 호출해서 데이터를 컨버팅 하고, 그중 이미지 데이터는 바이트코드로 변환하여 다양한 이미지 폼 (jpg, png등)과 이미지 리사이징을 거쳐 AWS S3에 업로드까지 하는 기능이다.

이중에서 외부 api 호출과 이미지 리사이징 등 다양한 I/O-bound 로직이 존재하기에 멀티쓰레드로 구현했을때의 속도개선을 기대할 수 있었다.

 

실제로 데이터 6개 기준 단일 쓰레드로 동작 시 약 96초 걸리던 동작이 두개 쓰레드로 동작해보니 약 48초로 단축된 걸 확인할 수 있었다.

 

단, 멀티쓰레드 방식을 도입하기엔 현재 두가지 문제점이 존재한다.

 

첫번째, CPU 사용량

멀티쓰레드로 동작시 해당 서버의 CPU 사용량이 단일쓰레드 대비 2배로 올랐다. (원래 단일 쓰레드 동작시에 5%이하였음)

테스트에서는 한명의 유저가 6개의 데이터를 동작한 것이지만 실제 운영환경에서는 백명의 사용자가 100개의 데이터 처리를 한번에 요청할 경우 현재 서버 스펙이 감당하지 못할 수 도 있을꺼라는 생각이 들었다.

좀더 안정적인 서비스 운영을 위해 기존 서버를 오토스케일링하게 하거나, 보다 자세히 기능 이용률을 파악하여 적절한 쓰레드 분배를 하는것이 필요해 보인다.

두번째, 외부 API의 호출제한

멀티쓰레드로 동작하는 내부 소스중에서는 쇼핑몰 api 를 호출하는 과정이 있다. 해당 api는 초당 5회의 호출 제한이 있기 때문에 호출량 조절이 아주 중요한데, 현재 단일쓰레드일 때에도 과한 호출 시 호출제한에 걸리는 경우가 있다.

이 상태에서 멀티쓰레드까지 적용하고 나면 호출량이 동일 시간 대비 쓰레드 갯수만큼 늘어나기 때문에 더욱 불안정해 질 것이다. 외부 호출 로직을 제외한 나머지 부분에서 멀티쓰레드를 적용해야 할까 싶다.

 

 

위 두가지 문제점을 해결 하기 위해 전체 과정 중 유난히 긴 시간을 소요하는 일부 로직에서만 멀티쓰레드로 구현하거나, 아님 이 부분을 비동기로 처리하거나 하는 해결방안을 생각해 보았다.

 

 
반응형
반응형

회사에서는 유저 행동에 대한 후처리 요청을 하는데 AWS의 SQS를 사용한다.

이미 입사했을때 SQS 설계가 다 되어있는 상태여서 초기 세팅은 알지 못했지만, 얼마 전 어떤 기능에서 메세지 전달관련하여 에러가 발생하는거 같아 조금 살펴보게 되었다.

결론적으론 SQS문제는 아니었고, 메세지를 전달받아 실행된 Lambda 프로젝트에서 KeyError 가 발생하여 병렬처리되는 Lambda가 불규칙적으로 중단되는 오류였다.

 

처음에 메세지 관련 문제일 것이라고 생각했던 이유가

1. 메세지 한개 전달 시 처리가 정상적으로 완료된다.

2. 메세지 두개 전달 시 처리가 정상적으로 완료된다.

3. 1과 2의 메세지 총 3개를 전달시 3개중 2개만 처리되고 하나는 처리중에서 무한대기 상태에 빠진다. (Lambda에서 Exception이 발생하여 남아있는 메세지를 미쳐 처리하지 못하게 된 상태)

 

위와 같은 문제가 발생하여 3개이상의 메세지 전달과정에서 오류가 있지 않았나 생각했었다. (결론은 아녔다..!!)


1. AWS의 메세징

AWS에서 제공하는 메세징 기능에는 SQS와 SNS가 있다. 흔히 Producer 로부터 메세지가 생성되어 메세징 서비스가 메세지를 전달받으면 이 내용을 Consumer에게 전달하여 작업이 처리되도록 한다. 이후 Consumer가 메세지를 생성하게 되면 메세징 서비스를 통해 Producer에게 다시 전달되기도 한다.

 

즉, 메세징 서비스는 요청한 곳과 요청을 받아 처리하는 곳의 전달자를 수행한다.

 

1) SQS (Simple Queue Service)

 

SQS는 Queue 기반 메세징 서비스로 선입선출 (FIFO) 로 메세지가 처리된다. 따라서 동시다발적으로 요청이 들어와도 요청 순서에 따라 순차적인 메세지 처리가 가능하다.

이러한 SQS는 한개의 Consumer에게만 메세지 전달이 가능하다.

만약 유저의 행동이 "구매" 와 "판매" 두가지 기능이 있다고 했을때, 이벤트 발생시 Lambda와 같은 프로젝트로 작업 처리 요청을 보내기 위해선 구매관련 SQS, 판매관련 SQS를 생성해야 한다.

 

이렇게 되면 구매와 판매 각각의 기능에 서로 병렬적으로 작업의 순차처리가 가능해진다.

ex)

구매1 -> 구매2 -> 판매1 -> 구매3 -> 판매2

 

위와 같은 유저 요청이 순차적으로 발생했다고 했을때 SQS가 한개에서 처리되었다면 요청들어온 순서대로 작업이 처리될 것이다.

하지만 구매와 판매의 SQS가 분리되어있기 때문에

구매 Queue : 구매1 -> 구매2 -> 구매3

판매 Queue : 판매1 -> 판매2

위와 같이 작업이 처리된다.

 

2) SNS (Simple Notification Service)

SNS역시 Consumer (구독자 라고 부르기도 한다) 에세 메세지를 전달하는 역활을 수행한다. 이때 Consumer는 Lambda와 같은 기능 뿐만이 아니라 SQS가 될수도 있고, 모바일이나 웹 자체에 알람을 보낼수도 있다.

 

SQS와 달리 SNS는 복수개의 Consumer를 가질 수 있다. 메세지 내용에 따라 서로 다른 Consumer에게로 메세지 전달이 가능하기 때문이다.

 

만약 위의 SQS예제에서 SNS를 사용하였다면 구매와 판매 각각의 메세징 서비스를 사용하지 않고, 발생하는 메세지는 모두 SNS로 보낸 다음 SNS에서 각각의 SQS로 메세지를 전달하여 최종적으로 요청이 처리되도록 구조를 설계할 수 있다.

 

이렇게 되면 SNS비용과 SQS비용이 모두 발생하기 때문에 한개의 SNS로 각 서비스에 요청을 처리하도록 하기도 한다.

 

2. 메세징 사용 이유

메세징 서비스는 기본적으로 비동기로 돌아간다. 때문에 작업 요청이 들어오고, 이 요청을 마무리 하기까지 시간이 오래걸리는 경우 작업 요청자는 결과가 나올때까지 기다려야 하는 과정이 없게 된다. (요청을 던지는 것이니까)

 

특정 조건에 대한 다량의 데이터를 조회하거나, 외부 api 를 호출하여 데이터를 가공하는 등과 같이 작업처리에 시간이 오래걸린다면 이러한 메세징 기반 서비스를 도입해 요청에 따른 처리를 비동기로 수행하게 하는것이 좋을 듯 하다.

 

물론 비동기로 처리하였을때 그 결과를 다시 확인하는건 필수..!!

(메시지 처리 결과를 요청을 처리한 곳에서 저장하도록 하고, 그 결과를 유저가 확인할 수 있다던가...)

 

+) 메세지 3개중 일부만 성공한 이유.. (파악중)

SQS, SNS와는 무관하지만 애초에 이 기능을 살펴보게 만든 이슈가 아직 의문이다.

해당 프로젝트는 Lambda로 구성되어있고, SQS로부터 전달받은 메세지는 특정 키 안에 list 형태로 되어있다. 어떤 요청을 처리하든지 공통 처리 부분에서 무조건 Key Error가 발생하게 되는데, 그렇다면 처음 요청에서 바로 에러가 발생해야 하는게 아닌가? 하는 생각을 하였다.

 

디버깅을 하면서 그 흐름을 따라가보니 정말로 첫번째 요청때도 KeyError가 발생하지만 이때에는 Exception이 일어나지 않고, 두번째 요청에서 또한번의 KeyError가 발생했을때 Exception이 발생한다.

반응형
반응형

코테연습을 하면서 '플로이드-워셜' 알고리즘을 활용한 문제를 마주했다.

처음에는 '플로이드-워셜' 알고리즘이 무엇인지 몰랐던 상태라 해당 알고리즘을 먼저 공부했고, 이를 문제에 활용했는데

개념은 알겠으나 왜 이렇게 적용하는지 이해가 되지 않은 부분이 있었다.

문제는 백준의 11403번 문제로 확인했다.

 

11403번: 경로 찾기

가중치 없는 방향 그래프 G가 주어졌을 때, 모든 정점 (i, j)에 대해서, i에서 j로 가는 길이가 양수인 경로가 있는지 없는지 구하는 프로그램을 작성하시오.

www.acmicpc.net


"""
3
0 1 0
0 0 1
1 0 0
"""
n = 3
city = [[0, 1, 0], [0, 0, 1], [1, 0, 0]]

1. 처음 풀이 : 오답

처음엔 "세개 노드를 확인하면서 첫번째와 세번째의 노드가 모두 두번째 노드와 연결되어있으면 첫번째와 세번째 노드도 연결 된것" 이라는 개념으로 아래와 같이 풀었었다.

def solution_my():
    """
    i에서 j의 경로를 탐색하면서 거쳐가는 k 노드의 값을 확인, k 값으로 연결된 i, j는 연결가능하다
    """
    for i in range(n):
        for j in range(n):
            for k in range(n):
                if city[i][k] == 1 and city[k][j] == 1:
                    city[i][j] = 1
    
""" 연결 체크 이후 답
0 1 1
1 1 1
1 1 1
"""

하지만 0에서 0으로 가는 경로가 체크되지 않았다. 0 -> 1 / 1 -> 2 / 2 -> 0 경로가 존재하기 때문에 0 -> 0 의 경로도 체크되야 한다.

위와같은 오류가 발생한 까닭은

0에서 연속적으로 연결된 노드 정보를 미쳐 파악하기 전에 노드 0의 연결정보 파악이 끝났기 때문이다.

i = 0 / j = 2 / k = 1 일때 i와 j의 연결정보를 갱신한다 citi[0] = [0 1 0] -> [0 1 1]

이후 i는 1로 갱신되어 0번째 노드의 탐색이 종료된다.

 

위 과정을 2번 반복하면 이전에 체크되지 않은 노드가 체크되어 정답이 된다.

for _ in range(2):
    for i in range(n):
        for j in range(n):
            for k in range(n):
                if city[i][k] == 1 and city[k][j] == 1:
                    city[i][j] = 1
"""
1 1 1
1 1 1
1 1 1
"""

 

하지만 2번확인해도 누락이 생긴다면? 이 방법으로는 오류를 근본적으로 해결할 수 없다.

다시 플로이드-워셜의 풀이로 왜 성공했는지 확인해보자.

2. 플로이드-워셜 풀이

def solution_floid():
    """
    i에서 j의 경로를 탐색하면서 거쳐가는 k 노드의 값을 확인, k 값으로 연결된 i, j는 연결가능하다
    """
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if city[i][k] == 1 and city[k][j] == 1:
                    city[i][j] = 1
""" 탐색 후 city 값
1 1 1
1 1 1
1 1 1
"""

내 풀이와 달리 모든 노드탐색이 확인되어 한번에 정답이 나왔다. (0에서 0으로 가는 경로도 체크되었다)

내 풀이와 위 풀이가 다른점은 탐색 노드 (k) 를 첫번째 for loop값으로 했다는 것이다.

즉, 시작노드가 i이고 종료노드가 j 일때 그 사이에 k 노드가 있는지 확인했던 내 풀이와 달리

탐색 노드 k를 기준으로 시작노드가 i일때 종료노드가 j이게 되는 수를 파악한 것이다.

 

플로이드-워셜 풀이를 기준으로 하면 가장 먼저 확인되는 추가 경로는 [2,1] 이다 해당 경로는 k 값이 0일때 2->0 이고, 0->1인게 확인되어 추가된다.

그 다음으로 추가되는 경로는 [0,2]이며 k값 1을 통해 경로가 확인된다. 위 방법의 풀이는 시작노드 값을 반복해서 확인하므로 내 풀이에서 0번 노드의 탐색을 바로 종료한것과 달리 k값을 탐색할때마다 확인하게 된다.

i값이 0이고, j값도 0일때 k 값에 따른 탐색 결과를 확인해 보면 다음과 같다.

#1 i=0, j=0, k=0 -> No
#2 i=0, j=0, k=1 -> No
"""
이때 i가 0이고, j가 2인 상황에서 k가 1일때 연결정보가 확인되므로
city[0][2] 값이 1로 갱신된다.
"""
#3 i=0, j=0, k=2 -> Yes
"""
위 과정으로 city[0][2]가 기존에 연결되지 않은 정보에서 갱신되었고,
city[2][0]은 원래 연결정보가 존재했으므로
city[0][0]에 대한 연결정보가 갱신된다.
"""

직접 풀이를 써내려가며 확인해 보니 왜 탐색노드를 첫번째 for loop 로 두었는지 감은 오지만, 아직까지 남에게 설명할 정도로의 감은 익히지 못한듯 하다. 추가로 플로이드-워셜 문제를 계속해서 풀면서 감을 익혀야 겠다.

!! 가중치가 주어진 상황 문제 풀어보기 !!

반응형
반응형

1. 자료형 초기 선언 두가지 방식 : 괄호 사용 or 내장 함수 사용

python 언어로 작업을 하면서 변수를 자료형의 초기상태로 선언할때 dictionary 는 중괄호 {} , list 는 대괗로 [] 로 표현했었다. 어느순간 작업하면서 얇은 괄호가 눈에 잘 띄지 않는 듯 하여 내장함수를 사용한 적도 있었다.
또한 python에서는 함수의 리턴 type을 명시할때도

def some_function() -> dict:
    ...

이런식으로 자료형의 "이름" 을 직접 명시하다 보니 자료형의 이름이 보이는것이 통일되게끔 작업한거 같기도 하다.
한개 함수는 항상 같은 자료형을 return 하는것이 좋기 때문에 조건에 맞는 데이터가 없어서 return 할 값이 없다 하더라도 None을 반환하지 않고 초기 자료형을 반환하도록 작업한다. 때문에 return 될 변수는 함수 첫 부분에 초기상태로 선언해둔다.

def some_function(a) -> dict:
    res = {} # 혹은 res = dict()
    if type(a) == int:
        res["type"] = "int"
    return res

만약 return 변수 res 를 초기에 선언하지 않았다면 if 문 안에서 res 변수를 선언하는 동시에 값을 담아 return 하고, if 문 바깥쪽에 예외상황의 return 을 선언해 주면 되긴 하다

def some_function(a) -> dict:
    res = {} # 혹은 res = dict()
    if type(a) == int:
        res = {"type": "int"}  # 혹은 res = dict(type='int')
        return res
    return {} # 혹은 return dict()

하지만 조건문이 여러개가 되고, 코드가 복잡해지면 return 이 빠지는 곳이 있을수 있어 모든 조건을 확인 한 후 마지막에 return 하는 방식으로 작업한다. 이때는, 딕셔너리 타입의 변수 res를 선언할때 res={} 이나 res=dict() 이 어떠한 차이가 있는줄 정확히 이해하지 못하였었다.

# 괄호를 이용하기

dict_var = {}
list_var = []
------------------------------
>>> print(dict_var, type(dict_var))
{} <class 'dict'>
>>> print(list_var, type(list_var))
[] <class 'list'>

# 내장함수 이용하기

dict_var2 = dict()
list_var2 = list()
------------------------------
>>> print(dict_var2, type(dict_var2))
{} <class 'dict'>
>>> print(list_var2, type(list_var2))
[] <class 'list'>

출력 결과를 보나, 타입을 보나 둘다 차이가 없어보인다. 하지만 python 의 timedit를 사용해 두가지의 변수 선언 방식의 시간 효율을 따져보니 차이가 발견되었다.

2. 선언 방식에 따른 실행 속도의 차이 : timeit

timeit 은 python에서 제공해주는 모듈로 timeit 이후의 명령어를 실행하는데 걸리는 시간을 확인해준다.

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)

stmt 은 timeit 으로 측정할 코드 및 함수 주체이며, setup stmt에 선언된 코드 및 함수를 실행하기 위해 필요한 사전 함수이다. 이때 실행속도 측정에 setup의 시간은 포함되지 않는다.
timer 함수로 Timer 인스턴스를 만드는데 default 값이 지정되어 있으며, number 실행으로 timeit() 메서드를 실행한다.
이때 number 인자도 default 값으로 10000000 이 지정된다. 또한 비필수 인자인 globals 는 코드를 실행할 이름 공간을 지정한다.

위 모듈을 통해 변수 초기선언 방식인 dict(){}의 속도차이를 확인해 보았다.

~ > python -m timeit "dict()"
10000000 loops, best of 3: 0.0451 usec per loop
~ > python -m timeit "{}"
100000000 loops, best of 3: 0.0115 usec per loop

단순 빈 자료형 선언인데도 속도가 무려 4배 넘게 차이가 났다.
그렇다면 변수를 초기 선언하면서 값을 세팅할때는 얼마나 나는지 1개 key-value 를 담아서 확인해보았다.

~ > python -m timeit "dict(fruit='apple')"
10000000 loops, best of 3: 0.0806 usec per loop
~ > python -m timeit "{'fruit': 'apple'}"
10000000 loops, best of 3: 0.0263 usec per loop

위 상황보다는 덜 나지만 비슷하게 4배정도 차이가 나는걸 확인할 수 있었다.



0.03초 차이가 크지 않아 보이지만 프로그래밍에서는 굉장한 차이를 의미한다.

따라서 변수를 선언할 때에도 이런 유의미한 차이를 이해하고 작성하면 조금더 효율적인 코드를 작성 할 수 있을 것이다.

반응형

+ Recent posts