본문 바로가기
Python

python 성능 최적화 코드 작성 방법

by pnnote 2023. 6. 30.
반응형

적절한 자료 구조 사용

Python에서 자료 구조를 적절하게 사용하는 것은 코드의 성능을 향상시킬 수 있는 중요한 방법이다. 예를 들면 Python에서는 리스트 컴프리헨션을 사용하는 것이 map 함수나 일반적인 for 루프를 사용하는 것보다 일반적으로 더 빠르다.

 

numbers = range(1, 1000000)

# for loop 사용
def square_for_loop(numbers):
    squares = []
    for n in numbers:
        squares.append(n * n)
    return squares

# map 함수 사용
def square_map(numbers):
    return list(map(lambda n: n * n, numbers))

# 리스트 컴프리헨션 사용
def square_comprehension(numbers):
    return [n * n for n in numbers]

 

또 다른 예로 문자열이 있다. Python에서는 문자열을 더하는 것이 대체로 비효율적이다. 문자열이 불변(immutable)하기 때문에 두 문자열을 더하면 새 문자열을 생성하고 기존 문자열을 복사해야 한다. 따라서 여러 문자열을 연결하려면 .join()을 사용하는 것이 더 효율적이다.

 

# 비효율적인 방법
def concatenate_plus(strings):
    result = ""
    for s in strings:
        result += s
    return result

# 효율적인 방법
def concatenate_join(strings):
    return "".join(strings)

 

로컬 변수 사용

Python에서 로컬 변수를 사용하는 것이 전역 변수를 사용하는 것보다 더 빠르다. 이유는 변수 접근 시간 때문이다. 변수를 찾는 과정에서 발생하는 차이 때문인데, 로컬 변수는 현재 함수의 네임스페이스에서 즉시 찾을 수 있지만, 전역 변수는 먼저 로컬 네임스페이스를 확인한 후 없으면 전역 네임스페이스를 확인한다. 이런 추가적인 확인 과정으로 인해 전역 변수 접근이 로컬 변수 접근보다 느려진다. 특히 반복문과 같은 곳에서 많은 차이를 만들어낸다. 예를 들어, 반복문에서 매번 전역 변수를 사용하면 해당 변수에 접근할 때마다 전역 네임스페이스를 확인해야 하므로, 반복문의 수행 속도가 느려질 수 있다. 전역 변수를 사용해야 하는 경우라면 사용하면 되지만 로컬 변수로 가능하다면 로컬 변수를 사용하거나 전역 변수를 로컬 변수로 복사하여 사용하는 것도 방법이다.

 

내장 함수와 표준 라이브러리 활용

일반적으로는 직접 함수를 구현하는 것보다 Python의 내장 함수와 표준 라이브러리를 사용하는 것이 빠르다. 더 빠른 이유는 이미 C로 최적화된 구현을 제공하기 때문이다. Python은 인터프리터 언어이기 때문에 실행 시간이 일반적으로 컴파일 언어보다 느린데, Python의 내장 함수들과 표준 라이브러리의 많은 부분들은 C 언어로 작성되어 있어 Python 코드보다 훨씬 빠르게 실행된다. 예를 들어, sum(), min(), max(), map(), filter() 등의 함수들은 모두 C로 작성되어 있어서 Python으로 같은 기능을 구현하는 것보다 더 빠르다.
표준 라이브러리의 모듈들도 마찬가지다. 일반적으로 잘 최적화되어 있어서 직접 코드를 작성하는 것보다 빠른 성능을 제공할 수 있다. itertools, functools, collections 등의 모듈들은 고성능의 자료 구조와 알고리즘이 구현되어 있다.

 

멀티스레딩/멀티프로세싱/비동기 프로그래밍

Python은 Global Interpreter Lock (GIL)이 있어서 멀티코어를 완전히 활용하는 것이 어렵다. 하지만 CPU-bound 작업을 병렬로 처리하는 경우, multiprocessing 모듈을 사용하여 프로세스를 분산시킬 수 있고, I/O-bound 작업을 최적화하려면 asyncio와 같은 비동기 프로그래밍을 사용할 수 있다. 멀티스레딩/멀티프로세싱/비동기 프로그래밍에 대한 설명은 다음과 같다.

 

멀티스레딩
멀티스레딩은 여러 작업을 동시에 실행하는 방법인데, 한 프로세스 내에서 여러 쓰레드가 동시에 실행되며, 모든 쓰레드가 동일한 메모리 공간을 공유한다. 이는 I/O 바운드 작업에서 유용하고, 하나의 쓰레드가 I/O 대기 상태일 때 다른 쓰레드가 계속해서 작업을 수행할 수 있다.

멀티프로세싱
멀티프로세싱은 여러 개의 프로세스를 동시에 실행하는 방법이다. 각 프로세스는 독립적인 메모리 공간을 가진다. 이 방법은 CPU 바운드 작업에 유용하고, 여러 개의 CPU 또는 CPU 코어를 효과적으로 활용할 수 있다. Python의 경우, GIL(Global Interpreter Lock)로 인해 하나의 쓰레드만이 한 번에 실행될 수 있지만, 멀티프로세싱을 사용하면 이 제약을 피할 수 있다.

비동기 프로그래밍
비동기 프로그래밍은 프로그램의 흐름을 블로킹하지 않고 여러 작업을 동시에 수행하는 방법이다. 일반적으로 이벤트 루프와 콜백, 프로미스, async/await 등의 메커니즘을 사용한다. 이 방법은 I/O 바운드 작업에서 특히 유용하며, 하나의 작업이 I/O 대기 상태일 때 다른 작업을 수행할 수 있다.

세 가지 방법 모두 작업의 실행을 병렬화하여 프로그램의 실행 시간을 단축시키지만, 각 방법은 특정한 상황에서 장점을 가진다. 예를 들어, 멀티스레딩과 비동기 프로그래밍은 I/O 바운드 작업에서 유용하고, 멀티프로세싱은 CPU 바운드 작업에서 유용하다. 상황에 맞는 방법을 선택하는 것이 중요하다.

 

 

Cython / C 확장 모듈 사용

파이썬 코드의 일부분을 C나 Cython으로 작성하는 것은 실행 속도를 크게 향상시킬 수 있다. C나 C++로 작성해야하는 번거로움이 있고 코드의 복잡성을 높일 수 있지만, 시스템 성능에 대한 요구사항이 매우 높은 경우에는 고려할만하다.

 

프로파일링

프로파일링은 코드의 어느 부분이 느린지 찾아내는 기능이다. cProfile이나 timeit과 같은 도구를 사용해서 코드의 병목현상을 찾아낼 수 있다. 다음은 python으로 cProfile을 사용하는 간단한 예제다.

 

import cProfile

cProfile.run('function(argument)')

 

cProfile.run 함수를 실행하면 함수 호출 수, 각 함수의 총 실행 시간, 호출당 평균 실행 시간 등의 세부 정보를 콘솔에 출력한다. 아래와 같이 실행하면 결과를 파일로 저장한다.

 

cProfile.run('function(argument)', 'outputfile.prof')

 

프로파일링 결과가 저장된 파일은 다음과 같이 불러와서 확인할 수 있다. 아래 코드는 가장 많은 실행 시간을 소비한 함수상위 10개를 확인하는 코드다.

 

import pstats

p = pstats.Stats('outputfile.prof')
p.sort_stats('tottime').print_stats(10)

 

 

반응형