由于某种原因,Table 类型(表格数据)的当前实现 Import 非常占用内存 - 效率低下。下面我尝试在一定程度上纠正这种情况,同时仍然重用 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
您可以看到,我的代码的内存效率比Import 高出大约 10 倍,同时速度也不慢。您可以通过调整chunkSize 参数来控制内存消耗。生成的表占用大约 150 - 200 MB 的 RAM。
编辑
更高效地处理稀疏表
我想说明如何使这个函数在导入过程中提高 2-3 倍的内存效率,再加上另一个数量级的内存效率(就您的最终内存占用而言)表,使用SparseArray-s。我们获得内存效率提升的程度很大程度上取决于您的表的稀疏程度。在您的示例中,该表非常稀疏。
稀疏数组的剖析
我们从一个用于构造和解构 SparseArray 对象的通用 API 开始:
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}};
一些简短的 cmets 是有序的。这是一个示例稀疏数组:
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-s 存储其元素的内部格式,并希望阐明上述函数的作用。
代码
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 MB RAM。我们可以将其与我们更通用的函数进行比较:
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