[Python] 메모리 사용 및 persistent dict, list Develop Tip

파이썬 스크립트를 이용하다 종종 embeded 등에서 구동시킬 경우도 있습니다.
그런데 기존에 서버 등에서 자유롭게 파이썬 코딩을 하던 것들도
embeded 등의 환경에서는 메모리가 많이 부족한 상황을 겪을 수도 있습니다.



이런 상황을 가정하고 다음과 같이 접근해 보았습니다.

1) 현재 실행되는 스크립트의 메모리 (Heap) 제한

def set_memory_limit(limit=1024):
limit *= 1024*1024 # MegaByte
rsrc = resource.RLIMIT_AS
# soft, hard = resource.getrlimit(rsrc) # defualt is -1,-1 (no limit)
resource.setrlimit(rsrc, (limit, limit)) #limit to one kilobyte
soft, hard = resource.getrlimit(rsrc)
return soft == limit and hard == limit

우선 위와 같은 resource 모듈의 setrlimit 을 이용하면 됩니다.
(처음에는 킬로바이트라고 읽은 것 같은데 나중에 보니 바이트 단위더군요)


2) 현재 프로그램이 사용하고 있는 메모리를 모니터링 합니다.

지난번 올렸던 메모리소비 구하기 와 비슷하지만 다른 방법으로 psutil 모듈을 이용해 보았습니다.

def getReadableSize(lv):
if not isinstance(lv, (int, long)):
return '0'
if lv >= 1024 * 1024 * 1024 * 1024:
s = "%4.2f TB" % (float(lv) / (1024 * 1024 * 1024 * 1024))
elif lv >= 1024 * 1024 * 1024:
s = "%4.2f GB" % (float(lv) / (1024 * 1024 * 1024))
elif lv >= 1024 * 1024:
s = "%4.2f MB" % (float(lv) / (1024 * 1024))
elif lv >= 1024:
s = "%4.2f KB" % (float(lv) / 1024)
else:
s = "%d B" % lv
return s

def print_heap(msg):
# return msg
rl = []
if msg:
# print msg,
rl.append(msg)
pid = os.getpid()
p = psutil.Process(pid)
# print(pid)
for map in p.memory_maps(grouped=False):
if '[heap]' in map.path:
rl.append(getReadableSize(map.pss))
break
return ' '.join(rl)


3) 이제 메모리를 많이 잡아 먹는 dict 를 테스트 해 봅니다

def big_dict_test():
sts = datetime.now()
bd = dict()
loop_limit = 200000
print(print_heap('before loop [%s]'%loop_limit))
for i in xrange(loop_limit):
try:
k = i
bd[k] = '%s%s'%(k, '*'*1024)
if i and i % 10000 == 0:
print(print_heap('Dict %s inserted...'%i))
except MemoryError:
print("[%s] error" % i)
return False
print(print_heap('after loop [%s]' % loop_limit))
print "bd[%s]=%s" % (loop_limit/2, bd[loop_limit/2])
for k in bd.keys():
del bd[k]
del bd
print(print_heap('after delete [%s]' % loop_limit))
ets = datetime.now()
print("big_list_test takes %s"%(ets-sts))
return True

적어도 1030 바이트 정도의 데이터와 int 색인을 넣은 dict 를 20만개 넣어
메모리 소비를 확인합니다.
대략 210MB 정도의 메모리를 사용합니다.

이제는 list 도 유사하게 테스트 합니다.

def big_list_test():
sts = datetime.now()
bl = list()
loop_limit = 200000
print(print_heap('before loop [%s]'%loop_limit))
for i in xrange(loop_limit):
try:
bl.append('%s%s'%(i, '*'*1024))
if i and i % 10000 == 0:
print(print_heap('List %s inserted...'%i))
except MemoryError:
print("[%s] MemoryError" % i)
return False
print(print_heap('after loop [%s]' % loop_limit))
print "bl[%s]=%s" % (loop_limit/2, bl[loop_limit/2])
del bl
print(print_heap('after delete [%s]' % loop_limit))
ets = datetime.now()
print("big_list_test takes %s"%(ets-sts))
return True

비슷한 데이터를 넣는데

특이하게도 dict 보다 더 메모리 소비가 큽니다. 약 214MB 소요되었습니다.

1의 제한을 걸고 위와 같은 함수 테스트를 진행하면
중간에 MemoryError 예외가 발생합니다.


4) Persistent dict, list 

기존 built-in dict 혹은 list 대신
pDict, pList 라는 클래스를 만들고 
기존의 dict와 list 와 거의 동일한 인터페이스를 제공한다고 가정합니다.
대신 pDict, pList 는 shelve (디폴트 bdb 이용) 표준 모듈을 이용해 봅니다.

class pDict(object):
#=====================================================================================
FOLER='/ferret/tmp'
INT_PREFIX='__int__'
#=====================================================================================
def _open(self):
if not os.path.isdir(self.FOLER):
self.FOLER = '/tmp'
if not os.path.isdir(self.FOLER):
raise IOError('Cannot wirte at folder "%s"' % self.FOLER)
while (True):
self.filename = '%s/.%08d.__dict__' % (self.FOLER, randint(1,99999999))
if not os.path.exists(self.filename):
break
self.d = shelve.open(self.filename)
#=====================================================================================
def _close(self, is_delete=True):
if self.d is not None:
self.d.close()
self.d = None
if is_delete and os.path.exists(self.filename):
os.remove(self.filename)
#=====================================================================================
def __init__(self, d={}):
self.filename = None
self.d = None
self._open()
if d and not isinstance(d, (dict, pDict)):
raise ReferenceError('pDict construct need only dict or pDict type but <%s>'
% str(type(d)))
for k, v in d.items():
self.__setitem__(k, v)
#=====================================================================================
def __del__(self):
self._close()
#=====================================================================================
def __repr__(self):
rl = list()
rl.append('{')
for i,k in enumerate(sorted(self.d.keys())):
if i > 0: rl.append(',')
rk = self.__r_keytransform__(k)
if isinstance(rk, basestring):
rk = '"%s"' % rk
rv = self.d[k]
if isinstance(rv, basestring):
rv = '"%s"' % rv
rl.append('%s:%s'%(rk,rv))
rl.append('}')
return ''.join(rl)
#=====================================================================================
def __getitem__(self, key):
if isinstance(key, slice):
return [ self.d[self.__keytransform__(k)]
        for k in range(key.start, key.stop, key.step) ]
return self.d[self.__keytransform__(key)]
#=====================================================================================
def __setitem__(self, key, value):
self.d[self.__keytransform__(key)] = value
#=====================================================================================
def __delitem__(self, key):
r = self.d[self.__keytransform__(key)]
del self.d[self.__keytransform__(key)]
return r
#=====================================================================================
def __iter__(self):
return iter(self.d)
#=====================================================================================
def __len__(self):
return len(self.d)
#=====================================================================================
def __keytransform__(self, key):
if isinstance(key, basestring):
return key
if isinstance(key, (int, long)):
return '%s%10d' % (self.INT_PREFIX, key)
return str(key)
#=====================================================================================
def __r_keytransform__(self, key):
if key.startswith(self.INT_PREFIX):
return int(key[len(self.INT_PREFIX):].strip())
return key
#=====================================================================================
def __contains__(self, key):
# return self.__keytransform__(key) in self.d
return self.has_key(key)
#=====================================================================================
def has_key(self, key):
return self.d.has_key(self.__keytransform__(key))
#=====================================================================================
def keys(self):
# for k in self.d.keys():
# yield self.__r_keytransform__(k)
return [ self.__r_keytransform__(k) for k in sorted(self.d.keys()) ]
#=====================================================================================
def values(self):
vl = list()
for k in sorted(self.d.keys()):
# yield self.d[k]
vl.append(self.d[k])
return vl
#=====================================================================================
def items(self):
for k in sorted(self.d.keys()):
yield self.__r_keytransform__(k), self.d[k]

class pList(pDict):
#=====================================================================================
def __init__(self, l=[]):
pDict.__init__(self)
self.len = 0
if l and not isinstance(l, (list, pList)):
raise ReferenceError('pList construct need only list or pList type but <%s>'
                    % str(type(l)))
for v in l:
self.append(v)
#=====================================================================================
def __repr__(self):
rl = list()
rl.append('[')
for i in xrange(self.len):
if i > 0: rl.append(',')
rv = self.d[self.__keytransform__(i)]
if isinstance(rv, basestring):
rv = '"%s"' % rv
rl.append('%s'%rv)
rl.append(']')
return ''.join(rl)
#=====================================================================================
def __setitem__(self, ndx, value):
if ndx < 0 or ndx > self.len:
raise IndexError('Invalid index <%s>' % ndx)
self.d[self.__keytransform__(ndx)] = value
#=====================================================================================
def __delitem__(self, ndx):
if ndx < 0 or ndx >= self.len:
raise IndexError('Invalid index <%s>' % ndx)
r = self.d[self.__keytransform__(ndx)]
del self.d[self.__keytransform__(ndx)]
for i in range(ndx, self.len-1):
self.d[self.__keytransform__(i)] = self.d[self.__keytransform__(i+1)]
self.len -= 1
if self.len > 0:
del self.d[self.__keytransform__(self.len)]
return r
#=====================================================================================
def __contains__(self, v):
return v in self.values()
#=====================================================================================
def __iter__(self):
return iter(self.values())
#=====================================================================================
def append(self, v):
self.__setitem__(self.len, v)
self.len += 1
#=====================================================================================
def extend(self, l):
for item in l:
self.append(item)
self.len += len(l)


5) 통합 테스트

파일과
파일을 동일 디렉터리에 다운받습니다.

우선 260MB 로 사용 메모리를 제한하고 실행하였습니다.

$ python limit_test.py -l 260
set_memory_limit(260)=1
before loop [200000] 1.38 MB
List 10000 inserted... 11.84 MB
List 20000 inserted... 22.13 MB
List 30000 inserted... 32.51 MB
List 40000 inserted... 42.88 MB
List 50000 inserted... 53.26 MB
List 60000 inserted... 63.64 MB
List 70000 inserted... 74.01 MB
List 80000 inserted... 84.39 MB
List 90000 inserted... 94.76 MB
List 100000 inserted... 105.14 MB
List 110000 inserted... 115.52 MB
List 120000 inserted... 125.89 MB
List 130000 inserted... 136.27 MB
List 140000 inserted... 146.64 MB
List 150000 inserted... 157.02 MB
List 160000 inserted... 167.39 MB
List 170000 inserted... 177.77 MB
List 180000 inserted... 188.15 MB
List 190000 inserted... 198.52 MB
after loop [200000] 208.90 MB
bl[100000]=100000****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
after delete [200000] 1.50 MB
big_list_test takes 0:00:00.364088
before loop [200000] 1.50 MB
Dict 10000 inserted... 12.72 MB
Dict 20000 inserted... 23.34 MB
Dict 30000 inserted... 33.22 MB
Dict 40000 inserted... 43.85 MB
Dict 50000 inserted... 54.47 MB
Dict 60000 inserted... 65.10 MB
Dict 70000 inserted... 75.72 MB
Dict 80000 inserted... 86.35 MB
Dict 90000 inserted... 96.97 MB
Dict 100000 inserted... 107.60 MB
Dict 110000 inserted... 118.23 MB
Dict 120000 inserted... 128.85 MB
Dict 130000 inserted... 139.48 MB
Dict 140000 inserted... 150.10 MB
Dict 150000 inserted... 160.73 MB
Dict 160000 inserted... 171.35 MB
Dict 170000 inserted... 181.98 MB
Dict 180000 inserted... 192.61 MB
Dict 190000 inserted... 203.23 MB
[190875] error

20만개를 채 끝내기 직전에 메모리 오류가 발생했습니다.
아마도 파이썬 자체가 60MB 정도 더 필요로 한다고 추측해 봅니다.


다음에는 280MB 로 제한하고 돌려 보았습니다.

$ python limit_test.py -l 280
set_memory_limit(280)=1
before loop [200000] 1.38 MB
List 10000 inserted... 11.84 MB
List 20000 inserted... 22.13 MB
List 30000 inserted... 32.51 MB
List 40000 inserted... 42.89 MB
List 50000 inserted... 53.26 MB
List 60000 inserted... 63.64 MB
List 70000 inserted... 74.02 MB
List 80000 inserted... 84.39 MB
List 90000 inserted... 94.77 MB
List 100000 inserted... 105.14 MB
List 110000 inserted... 115.52 MB
List 120000 inserted... 125.89 MB
List 130000 inserted... 136.27 MB
List 140000 inserted... 146.64 MB
List 150000 inserted... 157.02 MB
List 160000 inserted... 167.40 MB
List 170000 inserted... 177.77 MB
List 180000 inserted... 188.15 MB
List 190000 inserted... 198.53 MB
after loop [200000] 208.90 MB
bl[100000]=100000****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
after delete [200000] 1.50 MB
big_list_test takes 0:00:00.325896
before loop [200000] 1.50 MB
Dict 10000 inserted... 12.72 MB
Dict 20000 inserted... 23.35 MB
Dict 30000 inserted... 33.22 MB
Dict 40000 inserted... 43.85 MB
Dict 50000 inserted... 54.47 MB
Dict 60000 inserted... 65.10 MB
Dict 70000 inserted... 75.73 MB
Dict 80000 inserted... 86.35 MB
Dict 90000 inserted... 96.98 MB
Dict 100000 inserted... 107.60 MB
Dict 110000 inserted... 118.23 MB
Dict 120000 inserted... 128.85 MB
Dict 130000 inserted... 139.48 MB
Dict 140000 inserted... 150.11 MB
Dict 150000 inserted... 160.73 MB
Dict 160000 inserted... 171.36 MB
Dict 170000 inserted... 181.98 MB
Dict 180000 inserted... 192.61 MB
Dict 190000 inserted... 203.23 MB
after loop [200000] 213.86 MB
bd[100000]=100000****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
after delete [200000] 215.39 MB
big_list_test takes 0:00:00.251044

이 정도로 하니까 잘 동작했습니다.

이제는 100MB 로 제한하고 돌려볼까요?
물론 메모리 에러가 발생할 것입니다.

$ python limit_test.py -l 100
set_memory_limit(100)=1
before loop [200000] 1.38 MB
List 10000 inserted... 11.84 MB
List 20000 inserted... 22.13 MB
List 30000 inserted... 32.51 MB
List 40000 inserted... 42.88 MB
List 50000 inserted... 53.26 MB
[52346] MemoryError

50000 개 약간 상회하게 입력하다가 메모리가 모자르다고 나오네요...


마지막으로 persistent dict(pDict)와 list(pList)를 이용해 보도록 하겠습니다.

$ python limit_test.py -p -l 100
set_memory_limit(100)=1
before loop [200000] 1.73 MB
List 10000 inserted... 2.00 MB
List 20000 inserted... 2.03 MB
List 30000 inserted... 2.03 MB
List 40000 inserted... 2.03 MB
List 50000 inserted... 2.03 MB
List 60000 inserted... 2.03 MB
List 70000 inserted... 2.03 MB
List 80000 inserted... 2.03 MB
List 90000 inserted... 2.03 MB
List 100000 inserted... 2.03 MB
List 110000 inserted... 2.03 MB
List 120000 inserted... 2.03 MB
List 130000 inserted... 2.03 MB
List 140000 inserted... 2.03 MB
List 150000 inserted... 2.03 MB
List 160000 inserted... 2.03 MB
List 170000 inserted... 2.03 MB
List 180000 inserted... 2.03 MB
List 190000 inserted... 2.03 MB
after loop [200000] 2.03 MB
bl[100000]=100000****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
after delete [200000] 2.03 MB
big_list_test takes 0:00:05.463599
before loop [200000] 2.03 MB
Dict 10000 inserted... 2.03 MB
Dict 20000 inserted... 2.03 MB
Dict 30000 inserted... 2.03 MB
Dict 40000 inserted... 2.03 MB
Dict 50000 inserted... 2.03 MB
Dict 60000 inserted... 2.03 MB
Dict 70000 inserted... 2.03 MB
Dict 80000 inserted... 2.03 MB
Dict 90000 inserted... 2.03 MB
Dict 100000 inserted... 2.03 MB
Dict 110000 inserted... 2.03 MB
Dict 120000 inserted... 2.03 MB
Dict 130000 inserted... 2.03 MB
Dict 140000 inserted... 2.03 MB
Dict 150000 inserted... 2.03 MB
Dict 160000 inserted... 2.03 MB
Dict 170000 inserted... 2.03 MB
Dict 180000 inserted... 2.03 MB
Dict 190000 inserted... 2.03 MB
after loop [200000] 2.03 MB
bd[100000]=100000****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
after delete [200000] 9.29 MB
big_list_test takes 0:00:16.551439

우와 ...!
메모리를 거의 사용하지 않습니다.

대신 파일을 이용하므로 (BTree 특성상 항목이 많아질 수록 시간이 더 걸립니다)
메모리 만 사용할 때보다 60여배 느려지는 상황이 발생합니다.
(주어진 메모리 내에서 캐슁을 하는 등의 기능을 넣어볼 필요도 있겠습니다.)

지난번 살펴본 gc.collect() 후에도 메모리 남아있는 문제 와 더불어
메모리에 관하여 살펴보면 볼 수록 흥미롭습니다.


어느 분께는 도움이 되셨기를...




덧글

  • 제임스 2016/06/17 19:41 # 답글

    바로 옆에서 문창님의 실험을 바라보는 느낌이네요.

    요즘 Kalman Filter 공부를 하면서 python에 대한 생각이 조금씩 바뀌고 있습니다.
    jupyter나 science관련 library가 python에 참 잘되어 있다는 생각이 계속 들어
    첫 language로 python을 추천하는 이유를 피부로 느끼는 중이고요,
    아울러 문창님 조언대로 production을 python으로 하면 어떨까? 하는 생각까지 들어
    이번 글을 더 눈여겨 보게 되네요.
    고맙습니다

  • 지훈현서아빠 2016/06/18 17:00 #

    ㅎㅎ 저도 파이썬을 처음 시작하고서 3년쯤 지나 그런 생각이 시작되었구요~
    3년이상 저희 회사의 백엔드 컨트롤러 프레임워크로 정착을 시켜 나가고 있는데
    여러가지 힘에 부친다는 느낌을 많이 가지고 있습니다만 그래도 하고 있습니다.
    저는 사실 요즘 GO 에 더 매력을 느끼고 있습니다.
    물론 GLUE 언어로서의 파이썬이 대단히 쉽게 블록을 쌓기에 더 이상의 부족함을
    느끼지 못합니다만, GIL로 인한 쓰레드의 한계점 등과 여러명이 동시에 작업을 하다보면
    C와 같이 세세한 메모리 관리, 네이티브 코드에 비하여 40여배 정도나 느린 For Loop 등등
    또다른 한계가 마치 C와 파이썬을 묶은 듯한 느낌의 GO가 새로운 제시를 하고 있지 않나
    보여집니다.

    마치 Java가 나왔고 동일한 개념의 .NetFramework 가 스크립트와 컴파일언어 사이에 등장하였듯이
    이제는 스크립트와 같은 하이레벨 언어의 Feature 와 Compile된 native 코드가 섞인 형태 말이지요.
    GO와 동격이 Swift 같습니다. 앞으로의 대세가 되지 않을까 싶습니다.

    하여간 MicroServiceArchitecture 에 나오는 이야기가 polyglot 이라는 어느 언어로든 쉽고
    빠르게 구현하고 기능추가 및 변경도 빠르게 할 수있는 나름 대로의 개념이 언젠가는
    정착되지 않을까 싶기도 하구요.

    그런 의미에서의 Python은 의미를 계속 지니지 않을까 하는 생각입니다.
    주절 주절 두서없이 말을 해 보았습니다~
    행복한 주말 보내셔요~~
댓글 입력 영역

구글애드텍스트