Почему чтение всего файла занимает больше оперативной памяти, чем его размер на ДИСКЕ?

Предостережение

Это НЕ дубликат этого. Мне не интересно выяснять потребление памяти или в этом дело, так как я уже делаю это ниже. Вопрос в том, ПОЧЕМУ потребление памяти такое.

Кроме того, даже если мне действительно нужен способ профилирования моей памяти, обратите внимание, что guppy (предлагаемый профилировщик памяти Python в вышеупомянутой ссылке не поддерживает Python 3, а альтернатива guppy3 не дает точных результатов, что приводит к таким результатам, как (см. фактические размеры ниже):

Partition of a set of 45968 objects. Total size = 5579934 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  13378  29  1225991  22   1225991  22 str
     1  11483  25   843360  15   2069351  37 tuple
     2   2974   6   429896   8   2499247  45 types.CodeType

Фон

Итак, у меня есть этот простой скрипт, который я использую для некоторых тестов потребления ОЗУ, читая файл двумя разными способами:

  1. чтение файла построчно, его обработка и отбрасывание (через generators), что эффективно и рекомендуется практически для файлов любого размера (особенно больших файлов), работает должным образом.

  2. чтение всего файла в память (я знаю, что это не рекомендуется, однако это было только в образовательных целях).


Тестовый скрипт

import os
import psutil
import time


with open('errors.log') as file_handle:
    statistics = os.stat('errors.log')  # See below for contents of this file
    file_size = statistics.st_size / 1024 ** 2

    process = psutil.Process(os.getpid())

    ram_usage_before = process.memory_info().rss / 1024 ** 2
    print(f'File size: {file_size} MB')
    print(F'RAM usage before opening the file: {ram_usage_before} MB')

    file_handle.read()  # loading whole file in memory

    ram_usage_after = process.memory_info().rss / 1024 ** 2
    print(F'Expected RAM usage after loading the file: {file_size + ram_usage_before} MB')
    print(F'Actual RAM usage after loading the file: {ram_usage_after} MB')

    # time.sleep(30)

Выход

File size: 111.75 MB
RAM usage before opening the file: 8.67578125 MB
Expected RAM usage after loading the file: 120.42578125 MB
Actual RAM usage after loading the file: 343.2109375 MB

Я также добавил 30-секундный сон для проверки awk на уровне ОС, где я использовал следующую команду:

ps aux | awk '{print $6/1024 " MB\t\t" $11}' | sort -n

который дает:

...
343.176 MB      python  # my script
619.883 MB      /Applications/PyCharm.app/Contents/MacOS/pycharm
2277.09 MB      com.docker.hyperkit

Файл содержит около 800K копий следующей строки:

[2019-09-22 16:50:17,236] ERROR in views, line 62: 404 Not Found: The
following URL: http://localhost:5000/favicon.ico was not found on the
server.

Это из-за размеров блоков или динамического распределения, когда содержимое будет загружаться блоками, и большая часть этой памяти фактически не будет использоваться?


person Marius Mucenicu    schedule 24.09.2019    source источник
comment
В основном потому, что вы декодируете файл и сохраняете его в другой форме в оперативной памяти. Попробуйте, если open('errors.log', 'rb') имеет значение.   -  person L3viathan    schedule 24.09.2019
comment
Возможный дубликат Как мне профилировать использование памяти в Python?   -  person ivan_pozdeev    schedule 24.09.2019
comment
... также имейте в виду, что вы измеряете там полные процессы python, включая использование памяти интерпретатором python (парсер и среда выполнения). Но @L3viathan, скорее всего, прямо здесь.   -  person Raymond Nijland    schedule 24.09.2019
comment
ну .. Я имею в виду, что @L3viathan выглядит так, как будто ты прав. Если я открою его в rb, размер будет примерно таким же, хотя я хотел бы узнать об этом немного больше... если вы готовы к официальному ответу, я с удовольствием проголосую & принять.   -  person Marius Mucenicu    schedule 24.09.2019


Ответы (1)


Когда вы открываете файл в Python, по умолчанию вы открываете его в текстовом режиме. Это означает, что двоичные данные декодируются на основе значений операционной системы по умолчанию или явно заданных кодеков.

Как и все данные, текстовые данные представлены на вашем компьютере байтами. Большая часть английского алфавита может быть представлена ​​одним байтом, например. буква «А» обычно переводится как число 65 или в двоичном виде: 01000001. Эта кодировка (ASCII) достаточно хороша для многих случаев, но когда вы хотите написать текст на таких языках, как румынский, ее уже недостаточно, потому что символы ă, ţ и т. д. не являются частью ASCII.

Некоторое время люди использовали разные кодировки для каждого языка (группы), например. группа кодировок Latin-x (ISO-8859-x) для языков, основанных на латинском алфавите, и другие кодировки для других (особенно CJK) языков.

Если вы хотите представить некоторые азиатские языки или несколько разных языков, вам потребуются кодировки, кодирующие один символ в несколько байтов. Это может быть либо фиксированное число (например, в UTF-32 и UTF-16), либо переменное число, как в наиболее распространенной «популярной» сегодня кодировке UTF-8.


Вернемся к Python: строковый интерфейс Python обещает множество свойств, среди которых произвольный доступ со сложностью O(1), что означает, что вы можете очень быстро получить 1245-й символ даже из очень длинной строки. Это противоречит компактной кодировке UTF-8: поскольку один «символ» (на самом деле: одна кодовая точка Unicode) иногда имеет длину один, а иногда и несколько байтов, Python не может просто перейти к адресу памяти start_of_string + length_of_one_character * offset, поскольку length_of_one_character различается в UTF- 8. Поэтому Python должен использовать кодировку с фиксированной длиной байта.

Из соображений оптимизации он не всегда использует UCS-4 (~UTF-32), потому что это будет занимать много места, когда текст только в ASCII. Вместо этого Python динамически выбирает Latin-1, UCS-2 или UCS-4 для внутреннего хранения строк.


Чтобы собрать все вместе с примером:

Допустим, вы хотите сохранить в памяти строку "soluţie" из файла, закодированного как UTF-8. Поскольку для представления буквы ţ требуется два байта, Python выбирает UCS-2:

characters | s       | o       | l       | u       | ţ       | i       | e         
     utf-8 |0x73     |0x6f     |0x6c     |0x75     |0xc5 0xa3|0x69     |0x65
     ucs-2 |0x00 0x73|0x00 0x6f|0x00 0x6c|0x00 0x75|0x01 0x63|0x00 0x69|0x00 0x65

Как видите, для UTF-8 (файл на диске) требуется 8 байт, а для UCS-2 — 14.

Добавьте к этому накладные расходы на строку Python и сам интерпретатор Python, и ваши вычисления снова обретут смысл.


Когда вы открываете файл в двоичном режиме (open(..., 'rb')), вы не декодируете байты, а берете их как есть. Это проблематично, если в файле есть текст (поскольку для обработки данных вы рано или поздно захотите преобразовать их в строку, где затем вам придется выполнять декодирование), но если это действительно двоичные данные, такие как изображение, это нормально (и лучше).


Этот ответ содержит упрощения. Используйте с осторожностью.

person L3viathan    schedule 24.09.2019