Защо един речник използва толкова много RAM в Python

Написах скрипт на Python, който чете съдържанието на два файла, първият е сравнително малък файл (~30KB), а вторият е по-голям файл ~270MB. Съдържанието на двата файла се зарежда в структура от речникови данни. Когато се зареди вторият файл, бих очаквал необходимото количество RAM да бъде приблизително еквивалентно на размера на файла на диска, може би с малко допълнителни разходи, но като гледам използването на RAM на моя компютър, изглежда постоянно отнема ~2GB (около 8 пъти размера на файла). Съответният изходен код е по-долу (вмъкнати са паузи, за да мога да видя използването на RAM на всеки етап). Редът, който консумира големи количества памет, е "tweets = map(json.loads, tweet_file)":

def get_scores(term_file):
    global scores
    for line in term_file:
        term, score  = line.split("\t") #tab character
        scores[term] = int(score)

def pause():
    tmp = raw_input('press any key to continue: ')

def main():
    # get terms and their scores..
    print 'open word list file ...'
    term_file = open(sys.argv[1])
    pause()
    print 'create dictionary from word list file ...'
    get_scores(term_file)
    pause()
    print 'close word list file ...'
    term_file.close
    pause()

    # get tweets from file...
    print 'open tweets file ...'
    tweet_file = open(sys.argv[2])
    pause()
    print 'create dictionary from word list file ...'
    tweets = map(json.loads, tweet_file) #creates a list of dictionaries (one per tweet)
    pause()
    print 'close tweets file ...'
    tweet_file.close
    pause()

Някой знае ли защо е така? Притеснението ми е, че бих искал да разширя изследванията си до по-големи файлове, но бързо ще изчерпя паметта си. Интересното е, че използването на паметта не изглежда да се увеличава забележимо след отваряне на файла (тъй като мисля, че това просто създава указател).

Имам идея да опитам да превъртя файла един ред наведнъж и да обработя каквото мога и да съхраня само минимума, който ми е необходим за бъдещи справки, вместо да зареждам всичко в списък с речници, но просто ми беше интересно да видя дали приблизително 8 пъти множител на размера на файла в паметта при създаване на речник е в съответствие с опита на други хора?


person ChrisProsser    schedule 26.06.2013    source източник


Отговори (3)


Моето предположение е, че имате няколко копия на вашия речник, съхранени едновременно в паметта (в различен формат). Като пример, редът:

tweets = map(json.loads, tweet_file) #creates a list of dictionaries (one per tweet)

Ще създаде ново копие (+400~1000MB, вкл. речник, режийни). Но вашият оригинал tweet_file остава в паметта. Защо толкова големи цифри? Е, ако работите с Unicode низове, всеки Unicode знак използва 2 или 4 байта в паметта. Докато във вашия файл, приемайки UTF-8 кодиране, повечето знаци използват само 1 байт. Ако работите с обикновени низове в Python 2, размерът на низа в паметта трябва да е почти същият като размера на диска. Така че ще трябва да намерите друго обяснение.

РЕДАКТИРАНЕ: Действителният брой байтове, заети от „знак“ в Python 2, може да варира. Ето някои примери:

>>> import sys
>>> sys.getsizeof("")
40
>>> sys.getsizeof("a")
41
>>> sys.getsizeof("ab")
42

Както виждате, изглежда, че всеки знак е кодиран като един байт. Но:

>>> sys.getsizeof("à")
42

Не е за "френски" знаци. И ...

>>> sys.getsizeof("世")
43
>>> sys.getsizeof("世界")
46

За японски имаме 3 байта на знак.

Горните резултати зависят от сайта -- и се обясняват с факта, че моята система използва "UTF-8" кодиране по подразбиране. „Размерът на низа“, изчислен малко по-горе, всъщност е „размерът на байтовия низ“, представляващ дадения текст.

Ако „json.load“ използва „unicode“ низове, резултатът е по някакъв начин различен:

>>> sys.getsizeof(u"")
52
>>> sys.getsizeof(u"a")
56
>>> sys.getsizeof(u"ab")
60
>>> sys.getsizeof(u"世")
56
>>> sys.getsizeof(u"世界")
60

В този случай, както можете да видите, всеки допълнителен знак добавя 4 допълнителни байта.


Може би файловият обект ще кешира някои данни? Ако искате да задействате изрично делаокиране на обект, опитайте да зададете препратката му към Няма:

tweets = map(json.loads, tweet_file) #creates a list of dictionaries (one per tweet)
[...]
tweet_file.close()
tweet_file = None

Когато вече няма никаква препратка към даден обект, Python ще го делокира -- и така ще освободи съответната памет (от купчината на Python -- не мисля, че паметта се връща към системата).

person Sylvain Leroux    schedule 26.06.2013
comment
Пробвах това, но не се отразява на количеството използвана памет. Както бе споменато във въпроса, мисля, че обектът tweet_file всъщност е само указател към файла и не съхранява самите данни. - person ChrisProsser; 26.06.2013
comment
Съжалявам, грешката ми е: мислех, че сте прочели цялото съдържание на файла в tweet_file. Частта относно Unicode в отговора все още е актуална. - person Sylvain Leroux; 26.06.2013
comment
Между другото, когато говориш за използване на RAM, това наистина ли е RAM или виртуална памет? В зависимост от вашата операционна система, четенето на файл може просто чрез картографиране на страница с виртуална памет към действителния файл на диска. Така че използването на VM се увеличава, но няма реално увеличение на потреблението на памет. Все пак в зависимост от вашата операционна система, за I/O операция системата ще използва толкова RAM, колкото е налична за кеширане на данни. Не бъдете прекалено обсебени от числата. Реалността може да бъде по-сложна. - person Sylvain Leroux; 26.06.2013
comment
Благодаря, може би въпросът за символите в уникод може да помогне донякъде да обясни това. Ще го оставя отворен засега, в случай че има още отговори, и ще приема това, ако не. В RAM / виртуалната памет смятам, че използва физическа RAM, а не виртуална памет. Очевидно това е добро нещо за бърз достъп, но лошо за бързо изчерпване. - person ChrisProsser; 26.06.2013
comment
Не приемам обяснението на Unicode. Кодът очевидно е Python 2 (print изрази), а извикването open не указва и кодиране, така че низовете, прочетени от файла, ще бъдат байтови низове, а не низове в Unicode. - person ; 26.06.2013
comment
@delnan Направих редакция, за да изясня (малко) въпроса за размера на знаците в Python 2. - person Sylvain Leroux; 26.06.2013
comment
@SylvainLeroux FWIW, размерът на Unicode char се определя от опция по време на компилиране и json.loads изглежда винаги зарежда низове като Unicode. Вашето sys.getsizeof("à") е малко подвеждащо, тъй като зависи от кодирането на символите на вашата конзола. Ако използвате Latin-1, той ще бъде само един байт, но тъй като използвате UTF-8, всъщност въвеждате два „символа“ в низа, въпреки че можете да видите само един. - person Aya; 26.06.2013

Написах бърз тестов скрипт, за да потвърдя вашите резултати...

import sys
import os
import json
import resource

def get_rss():
    return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * 1024

def getsizeof_r(obj):
    total = 0
    if isinstance(obj, list):
        for i in obj:
            total += getsizeof_r(i)
    elif isinstance(obj, dict):
        for k, v in obj.iteritems():
            total += getsizeof_r(k) + getsizeof_r(v)
    else:
        total += sys.getsizeof(obj)
    return total

def main():
    start_rss = get_rss()
    filename = 'foo'
    f = open(filename, 'r')
    l = map(json.loads, f)
    f.close()
    end_rss = get_rss()

    print 'File size is: %d' % os.path.getsize(filename)
    print 'Data size is: %d' % getsizeof_r(l)
    print 'RSS delta is: %d' % (end_rss - start_rss)

if __name__ == '__main__':
    main()

...който отпечатва...

File size is: 1060864
Data size is: 4313088
RSS delta is: 4722688

...така че получавам само четирикратно увеличение, което би било отчетено от факта, че всеки Unicode знак заема четири байта RAM.

Може би бихте могли да тествате вашия входен файл с този скрипт, тъй като не мога да обясня защо получавате осемкратно увеличение с вашия скрипт.

person Aya    schedule 26.06.2013
comment
Благодаря, ще опитам това по-късно, когато отново имам достъп до файла. - person ChrisProsser; 26.06.2013
comment
Здравейте, имам малко затруднения с намирането на библиотеката с ресурси за Windows, някакви идеи? - person ChrisProsser; 27.06.2013
comment
@ChrisProsser О. Модулът resource съществува само в Linux. Ще трябва да премахнете функцията get_rss(). Нещата getsizeof все още трябва да работят. - person Aya; 27.06.2013
comment
Съжалявам за забавянето на отговора, моите резултати са Размерът на файла е: 277551773 и Размерът на данните е: 1070424362, което също е около четирикратно увеличение. Предполагам, че е мистерия къде отива другият GB. - person ChrisProsser; 03.07.2013

Обмисляли ли сте използването на паметта за ключовете? Ако имате много малки стойности в речника си, мястото за съхранение на ключовете може да доминира.

person Stefan    schedule 26.06.2013
comment
Очаквах малки режийни разходи за това, подобни на режийните разходи за съхранение, които бихте получили за индекс в база данни, но не очаквах това да бъде повече от ~20% от първоначалния размер на файла - person ChrisProsser; 26.06.2013
comment
Зависи от дължината на вашите линии. Ако ключовете са int, това са 8 байта на ред на 64-битова система. Ако редовете ви са дълги, това може да няма значение, но ако документът ви има много къси или празни редове, може да откриете, че клавишите са много по-големи от текста. - person Stefan; 26.06.2013