По какой-то причине текущая реализация Import
для типа Table
(табличные данные) довольно неэффективна в отношении памяти.Ниже я предпринял попытку несколько исправить эту ситуацию, в то же время используя многоуровневые возможности импорта Mathematica (через ImportString
).Для разреженных таблиц представлено отдельное решение, которое может привести к очень значительной экономии памяти.
Общее решение с эффективным использованием памяти
Вот гораздо более эффективная функция памяти:
Clear[readTable];
readTable[file_String?FileExistsQ, chunkSize_: 100] :=
Module[{str, stream, dataChunk, result , linkedList, add},
SetAttributes[linkedList, HoldAllComplete];
add[ll_, value_] := linkedList[ll, value];
stream = StringToStream[Import[file, "String"]];
Internal`WithLocalSettings[
Null,
(* main code *)
result = linkedList[];
While[dataChunk =!= {},
dataChunk =
ImportString[
StringJoin[Riffle[ReadList[stream, "String", chunkSize], "\n"]],
"Table"];
result = add[result, dataChunk];
];
result = Flatten[result, Infinity, linkedList],
(* clean-up *)
Close[stream]
];
Join @@ result]
Здесь я сопоставляю это со стандартом Import
для вашего файла:
In[3]:= used = MaxMemoryUsed[]
Out[3]= 18009752
In[4]:=
tt = readTable["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt"];//Timing
Out[4]= {34.367,Null}
In[5]:= used = MaxMemoryUsed[]-used
Out[5]= 228975672
In[6]:=
t = Import["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt","Table"];//Timing
Out[6]= {25.615,Null}
In[7]:= used = MaxMemoryUsed[]-used
Out[7]= 2187743192
In[8]:= tt===t
Out[8]= True
Вы можете видеть, что мой код примерно в 10 раз эффективнее памяти, чем Import
, хотяне намного медленнее.Вы можете контролировать потребление памяти, регулируя параметр chunkSize
.Ваша результирующая таблица занимает около 150 - 200 МБ ОЗУ.
РЕДАКТИРОВАТЬ
Еще более эффективна для разреженных таблиц
Я хочу показать, какможет сделать эту функцию еще в 2-3 раза более эффективной с точки зрения памяти во время импорта, а также еще на порядок более эффективной с точки зрения конечной памяти, занимаемой вашей таблицей, используя SparseArray
-s.Степень увеличения эффективности использования памяти во многом зависит от того, насколько разреженной является ваша таблица.В вашем примере таблица очень разреженная.
Анатомия разреженных массивов
Мы начнем с общепринятого API для создания и деконструкции объектов SparseArray
:
ClearAll[spart, getIC, getJR, getSparseData, getDefaultElement, makeSparseArray];
HoldPattern[spart[SparseArray[s___], p_]] := {s}[[p]];
getIC[s_SparseArray] := spart[s, 4][[2, 1]];
getJR[s_SparseArray] := Flatten@spart[s, 4][[2, 2]];
getSparseData[s_SparseArray] := spart[s, 4][[3]];
getDefaultElement[s_SparseArray] := spart[s, 3];
makeSparseArray[dims : {_, _}, jc : {__Integer}, ir : {__Integer},
data_List, defElem_: 0] :=
SparseArray @@ {Automatic, dims, defElem, {1, {jc, List /@ ir}, data}};
Некоторые краткие комментарии в порядке.Вот пример разреженного массива:
In[15]:=
ToHeldExpression@ToString@FullForm[sp = SparseArray[{{0,0,1,0,2},{3,0,0,0,4},{0,5,0,6,7}}]]
Out[15]=
Hold[SparseArray[Automatic,{3,5},0,{1,{{0,2,4,7},{{3},{5},{1},{5},{2},{4},{5}}},
{1,2,3,4,5,6,7}}]]
(я использовал цикл ToString
- ToHeldExpression
для преобразования List[...]
и т. Д. В FullForm
обратно в {...}
для удобства чтения).Здесь {3,5}
- это, очевидно, размеры.Далее 0
, элемент по умолчанию.Далее идет вложенный список, который мы можем обозначить как {1,{ic,jr}, sparseData}
.Здесь ic
дает общее количество ненулевых элементов при добавлении строк - поэтому сначала 0, затем 2 после первой строки, вторая добавляет еще 2, а последняя добавляет еще 3.В следующем списке, jr
, приведены позиции ненулевых элементов во всех строках, поэтому они равны 3
и 5
для первой строки, 1
и 5
для второй и 2
,4
и 5
для последнего.Нет никакой путаницы относительно того, где и где начинается и заканчивается строка, поскольку это может быть определено списком ic
.Наконец, у нас есть sparseData
, который представляет собой список ненулевых элементов, которые читаются построчно слева направо (порядок такой же, как для списка jr
).Это объясняет внутренний формат, в котором SparseArray
-ы хранят свои элементы, и, мы надеемся, проясняет роль указанных выше функций.
Код
Clear[readSparseTable];
readSparseTable[file_String?FileExistsQ, chunkSize_: 100] :=
Module[{stream, dataChunk, start, ic = {}, jr = {}, sparseData = {},
getDataChunkCode, dims},
stream = StringToStream[Import[file, "String"]];
getDataChunkCode :=
If[# === {}, {}, SparseArray[#]] &@
ImportString[
StringJoin[Riffle[ReadList[stream, "String", chunkSize], "\n"]],
"Table"];
Internal`WithLocalSettings[
Null,
(* main code *)
start = getDataChunkCode;
ic = getIC[start];
jr = getJR[start];
sparseData = getSparseData[start];
dims = Dimensions[start];
While[True,
dataChunk = getDataChunkCode;
If[dataChunk === {}, Break[]];
ic = Join[ic, Rest@getIC[dataChunk] + Last@ic];
jr = Join[jr, getJR[dataChunk]];
sparseData = Join[sparseData, getSparseData[dataChunk]];
dims[[1]] += First[Dimensions[dataChunk]];
],
(* clean - up *)
Close[stream]
];
makeSparseArray[dims, ic, jr, sparseData]]
Тесты и сравнения
Вот начальный объем используемой памяти (свежее ядро):
In[10]:= used = MemoryInUse[]
Out[10]= 17910208
Мы называем нашу функцию:
In[11]:=
(tsparse= readSparseTable["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt"]);//Timing
Out[11]= {39.874,Null}
Итак, это та же скорость, что и readTable
.Как насчет использования памяти?
In[12]:= used = MaxMemoryUsed[]-used
Out[12]= 80863296
Я думаю, это довольно примечательно: мы когда-либо использовали вдвое больше памяти, чем файл на диске, занимающий сам себя.Но, что еще более примечательно, конечное использование памяти (после завершения вычислений) значительно сократилось:
In[13]:= MemoryInUse[]
Out[13]= 26924456
Это потому, что мы используем SparseArray
:
In[15]:= {tsparse,ByteCount[tsparse]}
Out[15]= {SparseArray[<326766>,{9429,2052}],12103816}
Наша таблица занимает всего 12 МБ ОЗУ.Мы можем сравнить его с нашей более общей функцией:
In[18]:=
(t = readTable["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt"]);//Timing
Out[18]= {38.516,Null}
Результаты будут такими же, как только мы преобразуем нашу разреженную таблицу обратно в нормальную:
In[20]:= Normal@tsparse==t
Out[20]= True
, тогда как обычная таблица занимает гораздо большепробел (кажется, что ByteCount
переполняет занятую память примерно в 3-4 раза, но реальная разница все еще по крайней мере на порядок):
In[21]:= ByteCount[t]
Out[21]= 619900248