파이썬 파일 10만개 열고 닫는 것에 대한 메모리 사용량

allieclan의 이미지

with open(file_name, 'r'):
    text = f.read().lower()
    words = re.findall('[a-z]{1,}', text)
 
    words_dict[file_name] = words

반복문에서 위의 코드를 사용해 텍스트 파일의 영단어를 dict에 파일이름과 함께 저장하려고 합니다.
텍스트 파일의 총 용량이 500 MB도 되지 않는데, 이와 같은 코드를 파이참에서 실행하면 램 용량을 전부 사용하다가 결국 10만개의 연산을 하지 못하고 뻗어버리네요...
원인이 무엇인지 혹시 알 수 있을까요?

chanik의 이미지

총 500MB 정도의 텍스트파일이라도 단어별로 끊어 10만개의 리스트에 분산해 넣게 되면 총 메모리는 몇 기가바이트를 차지할 수 있습니다. 예를 들어 8바이트짜리 단어 640개가 들어있는 파일이 10만개 있다고 가정하면 총 6400만개의 단어가 되고 총 파일크기는 550MB 정도 되는데 대략 아래와 같은 공간을 차지하는 것 같습니다 (이론적 배경이 있는 것은 아니고 약간의 실험을 통해 추정한 것입니다).

- 10만개짜리 딕셔너리 크기 : 약 5MB
- 10만개의 키(문자열) 크기 : 키 길이가 평균 12바이트일때 6MB 정도
- 10만개의 값(리스트) 크기 : 640개짜리 리스트라면 총 520MB 정도
- 문자열의 총 크기 : 8바이트짜리 단어 6400만개라면 총 3.6GB 정도

단어를 담는 리스트와 단어 문자열이 차지하는 공간이 대부분인데, 4GB가 좀 넘게 되죠. 위 수치는 최악의 경우 즉 6400만개의 단어가 모두 서로 다른 상황에서이고, 만약 겹치는 단어가 많으면 그만큼 메모리 소비량은 줄어들겁니다. Ubuntu 20.04의 64-bit 파이썬 3.8.5 에서 테스트해보니 30초 정도면 10만개 파일을 읽어들여 딕셔너리 구성까지 끝나는군요. 메모리는 4GB 넘게 사용되고요.

이렇게 읽어들이기만 하는데도 문제가 생긴다면 아래의 가능성이 생각납니다.

ㅇ 혹시 32-bit 파이썬이면 프로세스당 메모리가 부족할 수 있겠고,
ㅇ 파이썬에서 문제가 없다면 pycharm에서 모종의 제약이 생길 수 있을 것 같습니다 (pycharm 써본적은 없고 그냥 추측입니다).

pycharm을 통하지 않고 바로 파이썬 인터프리터에서 직접 실행시켜도 문제가 생기는지, 만약 생긴다면 파일 갯수를 좀 줄여서 읽어들였을때 메모리 사용량이 어떤지 보는 식으로 찾아나가면 될 것 같네요.




2020-11-20 09:37 : 테스트해보고 틀렸음이 확인된 부분을 취소표시했습니다.

chanik의 이미지

질문글을 보고 호기심이 생겨서 좀 실험해보던 과정을 적어둡니다.

우선 아래와 같이 dict 디렉토리에, 8바이트짜리 단어 640개가 들어있는 데이터파일 10만개를 만든 뒤에,

$ seq -w 1 64000000 | split -l 640

질문하신 분의 코드를 조금 수정해 아래와 같이 읽어들였습니다.

$ python3
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> import re
>>> import glob
>>> import datetime
>>>
>>> datetime.datetime.now()
datetime.datetime(2020, 11, 20, 9, 58, 58, 350084)
>>> words_dict = {}
>>> for file_name in glob.glob("dict/*"):
...   with open(file_name, 'r') as f:
...     text = f.read().lower()
...     words = re.findall('[a-z0-9]{1,}', text)
...     words_dict[file_name] = words
...
>>> datetime.datetime.now()
datetime.datetime(2020, 11, 20, 9, 59, 23, 44041)
>>>
>>> sys.getsizeof(words_dict)
5242968
>>> sum([sys.getsizeof(fname) for fname in words_dict.keys()])
6063600
>>> sum([sys.getsizeof(words) for words in words_dict.values()])
548800000
>>> sum([sum([sys.getsizeof(w) for w in words]) for words in words_dict.values()])
3648000000

sys.getsizeof()로 몇 가지 테스트를 해보니 dict나 list는 안에 어떤 타입의 데이터가 들어있는지와 무관하게 멤버갯수에 의해서만 크기가 정해지더군요. 따라서 dict나 list가 차지하는 공간을 계산하려면 자체 공간과 함께 그 멤버들이 차지하는 공간을 따로 계산해 합쳐야한다는 결론이 나왔습니다. 그 결과로,

dict크기 + 키(fname)크기 + 값(리스트)크기 + 문자열(단어)크기
= 5242968 + 6063600 + 548800000 + 3648000000
= 4208106568 B
= 4109479 MB

위의 수치가 대략적인 메모리 사용량이라고 추측했습니다. 실행전후에 ps 해보면 아래와 같이 메모리 사용량이 달라졌고, 추측보다 500MB 정도 더 쓰였지만 제가 모르는 뭔가가 좀 더 있을뿐 추측이 어긋나진 않았다고 생각했고요.

$ ps -u
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
******   1827589  0.6  0.0  20368 10292 pts/1    S+   10:05   0:00 python3
 
$ ps -u
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
******   1827589 64.9 28.2 4637260 4627588 pts/1 S+   10:05   0:38 python3

제가 오해하는 부분이 있을수 있으니 과정을 정확히 적어야 섯부른 추측에 따른 오류 정정도 가능할 것 같아 올려둡니다.

익명_사용자의 이미지

일단, f.read.lower()는 읽어들인 파일컨텐츠만큼의 메모리를 두번 할당해버리는 일이 발생할꺼에요. 차라리 case-insensitive search로 메모리 할당을 줄이는것이 더 쌀꺼에요.

text = f.read()
words = re.findall('[a-zA-Z]{1,}', text)
words_dict[file_name] = map(str.lower, words)

그리고, str.lower()가 garbage collection을 도와주는 결과가 의외로 나올수 있어요. 다음의 *가정*을 만들어보죠.
1. f.read()는 해당 파일의 전체 내용을 단일 string buffer에 저장합니다.
2. re.findall()은 주어진 input string에서 주어진 패턴에 매칭하는 partial string들을 리턴합니다. 즉, 패턴에 매칭하는 부분들을 새로운 메모리로 복사를 해서 리턴을 하는것이 아닌, 매칭되는 부분의 포인터들을 리턴하는거죠.

(1) 과 (2)로 인하여 words_dict[filename] = words는 해당 파일의 전체내용이 저장된 메모리 버퍼 전체에 대한 reference count을 유지하게 되고, 새로운 파일을 열수록, 메모리 해제는 하나도 일어나지 않은채로 새로운 메모리 할당만 계속 되는 상황이 발생할수 있습니다.

어떻게 str.lower()가 garbage collection을 도와주냐고요? lower()는 주어진 string을 새로운 메모리로 복사해서 만들어지거든요. 기존의 코드는 f.read()로 읽어진 버퍼에 lower()를 바로 호출했기때문에, re.findall()이 리턴한 스트링들은 그 lower()로 만들어진 전체 메모리를 계속 참조하겠죠. 하지만, 제가 제시한 코드에서는 추출된 문자열들을 map(str.lower, words)를 통해 개별적인 복사본을 만들어서 words_dict에 넣습니다. 따라서, 파일을 읽는데 사용됐던 버퍼에 대한 reference count가 모두 없어지고, garbage collection이 일어날수 있게 도와줄수있습니다.

한번 해보시고 알려주세요.

여기부터는 서명입니다.
"저는 인터넷에서 숨어서 정확한 의견을 피력하는 자들과 말을 섞습니다."

chanik의 이미지

Quote:
다음의 *가정*을 만들어보죠.
1. f.read()는 해당 파일의 전체 내용을 단일 string buffer에 저장합니다.
2. re.findall()은 주어진 input string에서 주어진 패턴에 매칭하는 partial string들을 리턴합니다. 즉, 패턴에 매칭하는 부분들을 새로운 메모리로 복사를 해서 리턴을 하는것이 아닌, 매칭되는 부분의 포인터들을 리턴하는거죠.

여기서 가정1은 맞고 가정2는 틀립니다. re.findall()은 매칭된 문자열들의 포인터가 아닌 사본들을 돌려줍니다. 따라서 원본인 f.read() 결과물이 메모리에서 해제되어도 re.findall()로 찾은 문자열 리스트에는 영향이 없습니다.

파이썬3의 이미지

저도 아직 공부중이지만,,, 이거 나중에 좀 효율성 있는 코드 나오면 공유 부탁드릴께용^^^
무척 관심 가는 주제입니다~~~

[크롬북에서 적었어요~~~]

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.