25.12.09

Большой список строк


Стоит задача построчных манипуляций с большим файлом. Пробовал разные подходы. Оказалось, что попытки работы напрямую с файлом упираются в кэш файловой системы. Все эти tell-ы и seek-и выполняются быстро только если файл целиком влазит в этот самый кэш.

Поразмыслив я решил не увеличивать размер кэша. Во первых, память нужна не только этому скрипту. Во вторых работа с даже полностью закэшированным файлом всё же на порядок медленнее, чем с памятью напрямую. В общем, решил загонять весь файл в память.

Первое, что по этому поводу выяснилось, стандартный list питона кушает памяти в 2 раза больше, чем содержит данных. Я тестил на строчках требуемой длины (27 байт).

Вообще, мне хотелось бы даже сжать данные. Потому попробовал я записать bz2 файлик в tmpfs. Это ведь тоже работа с памятью. А потом считывать из него нужные строчки, там тоже можно сики делать. Это оказалось просто невероятно медленно.

Потом посмотрел в сторону mmap.  Даже если просто в память через неё писать, работает раза в два дольше, чем с закэшированным файлом. Либо кто-то из нас тормоз, либо одно из двух :)

Остановился на array. То, что получилось работает на скорости сравнимой с стандартным list-ом. Вот оно, собственно:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import array


def main():
    # тестим
    import random

    a = StringList()
    a.append(u'Утол')
    a.append(u'Мар')
    print len(a)
    print a[1].encode('utf-8')
    print random.choice(a).encode('utf-8')


class StringList(object):
    '''
    Экономичный к памяти список строк
    '''
    def __init__(self, encoding='cp1251'):
        self.data = array.array('c')
        self.index = array.array('L')
        self.encoding = encoding

    def append(self, s):
        self.index.append(len(self.data))
        self.data.fromstring(s.encode(self.encoding))

    def __getitem__(self, id):
        start = self.index[id]
        try:
            end = self.index[id + 1]
        except IndexError:
            end = len(self.data)
        return self.data[start:end].tostring().decode(self.encoding)

    def __len__(self):
        return len(self.index)


if __name__ == "__main__":
    main()