[출처: https://blog.jetbrains.com/pycharm/2025/11/10-smart-performance-hacks-for-faster-python-code/ ]
1. 내부항목 검사(Membership Test)에 효과적인 Set(집합) 활용하기
배열형 자료형의 내부항목을 검사는 다음과 같은 "내부항목 in 배열형" 연산자와 if문을 조합하여 처리한다.
하지만, 가장 손쉽게 사용하는 list를 검색하는 것은 특히 비효율적이다. list의 크기가 커질수록 내부항목검사(x in some_list)는 각 내부항목을 하나씩 스캔해서 비교해야 하므로 S/W time complexity는 선형 시간 복잡도 (O(n))를 가집니다(여기서, n은 내부항목의 개수).
위와 같이 최악의 경우를 상정한 것이기는 하지만, 총 1M 개를 내부항목으로 갖는 list와 set의 내부항목 검사에 소요되는 시간은 다음과 같다.
True
List lookup: 0.005432s
True
Set lookup: 0.000081s
파이썬의 set은 내부항목 관리를 해시 테이블로 구현되어 평균적으로 상수 시간(O(1)) 조회 성능을 제공하여 특히 대규모 데이터셋을 다룰 때 set 내 값 존재 여부 확인할때 훨씬 빠르다.
통상적으로 set보다 list를 고민없이 사용하는 경우들이 많은데 set가 list보다 훨씬 효율적이다; set는 내부항목 검사 속도를 높일 뿐만 아니라 합집합, 교집합, 차집합과 같은 연산을 훨씬 빠르고 간결하게 수행합니다.
내부항목 검사에 list 대신 set를 사용하면(특히 성능이 중요한 코드에서) 로직을 최소한으로 변경하면서도 상당한 속도 향상을 달성할 수 있다.
2. 불필요한 복사 행위를 피하라.
list, dict 또는 array 같은 큰 객체를 복사하는 것은 시간과 메모리 측면에서 비용이 많이 들 수 있다. 복사 작업은 메모리에 새로운 객체를 생성하며, 이는 특히 대규모 데이터셋을 다루거나 많은 반복 작업을 수행해야하는 반복문 내에서 작업할 때 상당히 부담스러운 오버헤드가 된다.
가능한 경우 객체를 복제하지 말고 그 자리에서 수정하여 새 구조체를 할당하고 채우는 오버헤드를 피하자.(메모리 사용량을 줄이고 성능을 향상시킵니다.) 파이썬의 많은 내장 데이터 구조들은 복사 작업이 필요 없는 인플레이스 메서드(예: sort, append, update)를 제공하고 이를 적극 활용하자.
In-place: 0.0000s
Copy: 0.0049s
성능이 중요한 코드에서는 객체가 언제, 어떻게 복제되는지 주의 깊게 살펴보고 reference(new_list = list)와 in-place 연산을 활용하면 크거나 복잡한 데이터 구조를 다룰 때 더 효율적이고 메모리 친화적인 코드를 작성할 수 있다.
3. 메모리 효율성을 위해서 객체의 구성요소관리에 __slots__ 를 사용하자.
기본적으로 Python 클래스는 인스턴스 속성을 동적 사전(__dict__)에 저장한다. 이는 유연성을 제공하지만 메모리 오버헤드가 발생하고 속성 접근 속도가 약간 느려진다.
__slots__를 사용하면 클래스에 대해 고정된 속성 집합을 명시적으로 선언할 수 있습니다. 이는 __dict__의 필요성을 없애 메모리 사용량을 줄여주며, 특히 클래스의 인스턴스를 많이 생성할 때 유용합니다. 또한 간소화된 내부 구조 덕분에 속성 접근 속도가 약간 빨라집니다.
__slots__는 동적 속성 할당을 제한하지만, 메모리 제약 환경이나 성능에 민감한 애플리케이션에서는 효율적이다. 경량 클래스나 데이터 컨테이너의 경우 __slots__를 적용하는 것이 코드를 더 효율적으로 만드는 간단한 방법이다.
하지만, 결과는 들쑥 날쑥이다. 우선 이론상 동적 메모리 관리가 정적 메모리 관리보다 훨씬 시간이 더 든다는 것은 당연한 사실이니 나름 합리적인 내용이다. 하지만 결과는 들쑥날쑥....
Without slots: 0.6076s
With slots: 0.5399s
...
Without slots: 0.4644s
With slots: 0.6735s
마무튼 이 내용은 보류.
4. 기본 연산자 대신 math 패키지를 사용하자.
수치 계산의 경우, Python의 math 모듈은 C로 구현된 함수를 제공하여 순수 Python으로 작성된 동일한 연산보다 더 나은 성능과 정밀도를 제공한다.
예를 들어, math.sqrt()를 사용하는 것이 지수 연산자(**)를 사용하여 숫자를 0.5제곱하는 것보다 일반적으로 더 빠르고 정확합니다. 마찬가지로, math.sin(), math.exp(), math.log()와 같은 함수들은 속도와 신뢰성을 위해 고도로 최적화되어 있습니다.
이러한 성능 향상 효과는 특히 반복 작업을 수행하는 반복문이나 대규모 계산에서 두드러지게 나타난다. 수치 연산이 많은 작업에 수학 모듈을 활용하면 더 빠른 실행 속도와 더 일관된 결과를 동시에 달성할 수 있어, 과학적 계산, 시뮬레이션 또는 수학 연산이 많은 코드에 최적의 선택이다.
Math sqrt: 0.4781s
Operator: 0.6468s
5. 공간의 크기가 명확히 정해져 있다면 메모리를 사전에 할당해서 사용하자.
동적으로 list나 array을 생성할 때 파이썬은 내부적으로 크기가 커짐에 따라 자동으로 크기를 조정한다. 편리하지만, 이러한 크기 조정은 새로운 크기의 메모리 할당과 데이터 복사 그리고 기존의 메모리 반납을 수반하여 오버헤드를 발생시킨다. 특히 대규모 루프나 성능이 중요한 루프에서 더욱 그렇다.
데이터 구조의 최종 크기를 미리 알고 있다면 매번 필요시에 동적할당을 수행하는 방식이 아닌 사전에 메모리를 미리 할당해서 사용하여 성능 향상시킬 수 있다. list나 array를 고정된 크기로 초기화하면 반복적으로 발생할 수 있는 (동적할당->복사->메모리반환) 크기 조정을 피할 수 있으며, 파이썬(또는 NumPy 같은 라이브러리)이 메모리를 더 효율적으로 관리할 수 있다.
이 기법은 수치 계산, 시뮬레이션, 대규모 데이터 처리에서 특히 유용하며 이런 작은 최적화조차도 연쇄적인 누적 효과를 낼 수 있다. 메모리를 사전 할당해서 사용하면 메모리 파편화(Memory Fragmentation)를 줄이고 캐시 지역성을 개선하며 더 예측 가능한 성능을 보장한다.
파이썬의 예외 처리는 예상치 못한 동작을 관리하는 데 강력하고 깔끔하지만, 성능이 중요한 루프 내에서 적합하도록 설계된 것은 아니다. 예외 발생 및 포착에는 스택 추적(unwinding)과 컨텍스트 전환이 수반되며, 이는 상대적으로 비용이 많이 드는 작업이다.
hot loop(반복적으로 실행되거나 대량의 데이터를 처리하는 코드 구간)에서 제어 흐름을 위해 예외를 사용하면 성능이 크게 저하된다. 대신 오류가 발생하기 전에 이를 방지하기 위해 조건부 검사(if, in, is 등)를 사용하면, 훨씬 빠를 뿐만 아니라 더 예측 가능한 실행을 수행한다.
예상되는 제어 흐름보다는 진정으로 예외적인 경우에만 예외를 허용하면, 특히 성능이 중요한 타이트한 루프나 실시간 애플리케이션에서 더 깔끔하고 빠른 코드를 얻을 수 있다.
Conditional: 0.8241s
Exception: 1.3350s
7. 반복 수행과정에서 local 함수를 사용하자.
함수 내에서 특정 논리 블록이 반복적으로 수행해야 할 경우, 이를 지역(중첩) 함수(closure라고도 함)로 정의하면 성능과 가독성을 향상시킬 수 있다. 지역 함수는 이름 탐색 속도가 더 빠릅니다. 당연하게도 파이썬이 전역 범위보다 지역 범위에서 변수 혹은 함수를 조회하는 것이 더 빠르기 때문이다.
이 기법은 루프, 데이터 변환 또는 재귀적 프로세스와 같이 동일한 작업을 여러 번 적용하는 함수에서 특히 유용하고, 자주 사용되는 로직을 로컬로 유지함으로써 런타임 오버헤드와 인지 부하를 모두 줄일 수 있다.
Local function: 0.3879s
Global function: 0.6965s
8. 조합 연산에는 itertools를 사용하자.
개인적인 경험상 이를 통해 잃을 수 있는 손해가 더 많다고 생각하고 실행 속도의 실요성도 없어 보여서 제외함.
9. 정렬된 list 다룰 때 bisect를 사용하자.
정렬된 목록을 다룰 때 선형 검색이나 수동 삽입 로직을 사용하면 비효율적이다. 특히 목록이 커질수록 더욱 심해진다. Python의 bisect 모듈은 이진 검색을 사용하여 정렬된 순서를 유지하는 빠르고 효율적인 도구를 제공한다.
bisect_left(), bisect_right(), insort() 같은 함수를 사용하면 단순 스캔의 O(n) 복잡도와 달리 O(log n) 시간에 삽입 및 검색을 수행할 수 있다. 이는 리더보드 유지 관리, 이벤트 타임라인 관리, 효율적인 범위 쿼리 구현 같은 시나리오에서 특히 유용하다.
bisect 모듈을 사용하면 list 변경 후 매번 재정렬할 필요가 없어지며, 동적 정렬 데이터 작업 시 상당한 성능 향상을 얻을 수 있습니다. 이는 가볍고 강력한 도구로, 일반적인 list 작업에 알고리즘적 효율성을 제공합니다.
Bisect: 0.0004s
Loop: 0.0027s
10. 루프 내에서 반복된 함수 호출을 피하라.
이해할 수 없어서 개인적으로 제외한다.
요약
itertools, bisect, collections와 같은 특수 모듈은 복잡한 작업을 더욱 간소화하며, 전역 변수 사용 최소화, 메모리 사전 할당, 캐싱 구현과 같은 모범 사례 준수는 간결하고 효율적인 코드 실행을 보장합니다.
댓글 없음:
댓글 쓰기