
파이썬(Python) 기반의 데이터 과학 및 딥러닝 프로젝트를 진행하다 보면, 복잡한 신경망 모델이나 대규모 텐서(Tensor) 객체를 복사해야 하는 상황을 자주 마주하게 됩니다. 이때 가장 먼저 떠오르는 도구는 표준 라이브러리의 copy.deepcopy()입니다. 하지만 수 GB 단위의 텐서 데이터에서 이 함수를 호출하는 순간, 프로그램은 응답을 멈추고 메모리 점유율은 수직 상승하게 됩니다.
본 포스팅에서는 왜 copy.deepcopy()가 대규모 텐서에서 유독 느린지 그 내부 메커니즘을 분석하고, 실무 개발 현장에서 즉시 적용 가능한 7가지 고성능 최적화 해결 방법을 심도 있게 다룹니다.
1. 왜 copy.deepcopy는 대규모 텐서에서 느린가?
copy.deepcopy()는 파이썬의 모든 객체를 재귀적으로 탐색하며 복사합니다. 이 과정에서 객체 메모(Memoization)라는 메커니즘을 사용하는데, 이미 복사된 객체를 추적하여 순환 참조를 방지합니다. 하지만 수백만 개의 요소를 가진 텐서 객체에서는 이 재귀적 탐색과 메모이제이션 과정이 엄청난 오버헤드를 발생시킵니다. 특히 PyTorch나 TensorFlow와 같은 라이브러리의 텐서는 단순한 파이썬 객체가 아니라 C++ 수준에서 관리되는 메모리 뷰(View)를 가지고 있습니다. deepcopy는 이러한 저수준 최적화를 무시하고 파이썬 인터프리터 수준에서 복사를 시도하기 때문에 성능 저하가 필연적입니다.
2. 전략별 성능 비교 요약
다음은 대규모 텐서 복사 시 사용할 수 있는 주요 전략들을 비교한 표입니다.
| 방법론 | 주요 메커니즘 | 성능(속도) | 메모리 효율 | 권장 상황 |
|---|---|---|---|---|
| Standard deepcopy | 재귀적 전체 객체 복사 | 매우 느림 | 매우 낮음 | 소규모 일반 객체 |
| Framework Clone | C++ 수준 메모리 복사 | 매우 빠름 | 높음 | 단일 텐서 복사 |
| In-place Sharing | 메모리 주소 공유 (View) | 즉시 완료 | 최상 | 읽기 전용 작업 |
| Serialization | 바이트 스트림 변환 후 복원 | 보통 | 보통 | 프로세스 간 전송 |
| State Dict Copy | 파라미터 가중치만 복사 | 빠름 | 매우 높음 | 모델 가중치 백업 |
3. 실무 적용 가능한 고성능 Example 7가지
개발자가 자신의 환경에 맞춰 즉시 복사하여 사용할 수 있는 실무 코드를 소개합니다.
Example 1: PyTorch 전용 clone() 및 detach() 조합
PyTorch 사용자라면 deepcopy 대신 엔진 내부에서 최적화된 clone()을 사용해야 합니다.
import torch
def fast_tensor_copy(tensor):
# clone()은 메모리를 새로 할당하고 내용을 복사하며,
# detach()는 계산 그래프에서 분리하여 메모리 누수를 방지합니다.
return tensor.clone().detach()
# 사용 예시
large_tensor = torch.randn(10000, 10000)
copied_tensor = fast_tensor_copy(large_tensor)
Example 2: NumPy View를 활용한 제로 카피(Zero-copy)
데이터를 수정하지 않고 구조만 변경하거나 읽기만 한다면 메모리를 복사할 필요가 없습니다.
import numpy as np
def get_readonly_view(array):
# 새로운 객체를 생성하지만 원본 버퍼를 공유합니다.
view = array.view()
view.flags.writeable = False
return view
large_data = np.zeros((5000, 5000))
safe_view = get_readonly_view(large_data)
Example 3: State Dictionary를 이용한 모델 가중치 고속 복사
전체 모델 객체를 deepcopy하는 대신, 학습에 필요한 파라미터(Weight)만 추출하여 복사하는 방법입니다.
import torch.nn as nn
import io
def shadow_copy_model(model):
# 가중치 딕셔너리만 CPU 메모리에서 고속으로 복사합니다.
buffer = io.BytesIO()
torch.save(model.state_dict(), buffer)
buffer.seek(0)
new_model = type(model)() # 동일 클래스의 새 인스턴스 생성
new_model.load_state_dict(torch.load(buffer))
return new_model
Example 4: 다차원 배열의 슬라이싱 복사 최적화
텐서의 특정 부분만 복사할 때 copy() 메서드를 명시적으로 호출하여 메모리 연속성을 보장합니다.
def copy_sub_tensor(tensor, start_idx, end_idx):
# 슬라이싱 후 copy()를 호출하면 해당 부분만 새로운 메모리 블록에 할당됩니다.
return tensor[start_idx:end_idx].clone()
Example 5: 공유 메모리(Shared Memory)를 이용한 프로세스 간 복사
멀티프로세싱 환경에서 대규모 텐서를 복사할 때 발생하는 직렬화 오버헤드를 피하는 방법입니다.
import torch.multiprocessing as mp
def move_to_shared_memory(tensor):
# 텐서를 공유 메모리로 이동시켜 프로세스 간 복사 없이 접근 가능하게 합니다.
return tensor.share_memory_()
Example 6: pickling 최적화 (Pickle Protocol 5)
객체를 직렬화하여 복사할 때 최신 파이썬 프로토콜을 사용하여 대용량 데이터를 처리합니다.
import pickle
def fast_pickle_copy(obj):
# Protocol 5는 out-of-band 데이터를 지원하여 대규모 수치 데이터에 최적화되어 있습니다.
return pickle.loads(pickle.dumps(obj, protocol=5))
Example 7: 커스텀 __deepcopy__ 메서드 구현을 통한 선택적 복사
클래스 내부에 __deepcopy__를 직접 정의하여, 무거운 텐서 데이터는 clone()으로 처리하고 나머지는 일반 복사를 수행하게 제어합니다.
class NeuralNetworkNode:
def __init__(self, weights):
self.weights = weights
self.metadata = {"version": 1.0}
def __deepcopy__(self, memo):
# 텐서는 최적화된 방식으로, 일반 속성은 표준 방식으로 복사
new_weights = self.weights.clone()
new_obj = self.__class__(new_weights)
memo[id(self)] = new_obj
return new_obj
4. 결론 및 성능 최적화 제언
대규모 텐서 객체를 다룰 때 copy.deepcopy()를 무분별하게 사용하는 것은 소프트웨어의 성능을 갉아먹는 주요 원인이 됩니다. 핵심은 "파이썬의 일반 객체 계층"과 "저수준 라이브러리(C++/CUDA)의 데이터 계층"을 분리해서 생각하는 것입니다.
가장 권장되는 해결 방법은 프레임워크가 제공하는 전용 메서드(.clone(), .copy())를 사용하는 것이며, 모델 복제 시에는 state_dict를 활용한 가중치 전송 방식을 채택하는 것입니다. 이러한 접근 만으로도 복사 속도를 최소 10배에서 최대 100배 이상 개선할 수 있습니다.
5. 참고 문헌 및 출처
- Python Software Foundation. "copy — Shallow and deep copy operations." docs.python.org.
- PyTorch Documentation. "Tensor.clone() vs Tensor.detach()." pytorch.org/docs.
- NumPy Manual. "Copies and Views." numpy.org/doc.
- Effective Python by Brett Slatkin, Item 53: Use copy.deepcopy sparingly for large data.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 메타클래스(Metaclass)로 신경망 인터페이스를 강제하는 3가지 방법과 설계 해결책 (0) | 2026.04.22 |
|---|---|
| [PYTHON] Pickle 대신 MessagePack과 Protobuf를 사용하는 3가지 이유와 성능 차이 해결 방법 (0) | 2026.04.22 |
| [PYTHON] sys.getsizeof가 메모리 할당량을 정확히 측정 못하는 3가지 이유와 해결 방법 (0) | 2026.04.22 |
| [PYTHON] 데코레이터 7가지를 활용한 ML 실험 로깅 표준화 및 실행 시간 추적 방법 (0) | 2026.04.22 |
| [PYTHON] Dataclasses와 Pydantic V2의 대규모 데이터 처리 성능 차이와 7가지 최적화 방법 (0) | 2026.04.22 |