请在2024年Clojure调查中分享您的想法!

欢迎!请查阅关于页面以了解更多该功能的工作方式。

0
Clojure
编辑

你好,

我正在尝试解析一个50MB的CSV文件。大约2500行,大约5500列,其中一列是字符串(日期为yyyy-mm-dd),其余的是浮点数,有很多空值。我需要能够访问所有数据,因此希望完整地实现文件,在这个大小应该是可行的。

我尝试了几种不同的方法,从

(with-open [rdr (io/reader path)] (doall (csv/read-csv rdr))))

到使用line-seq和手动解析字符串成为数字的稍微更手动的方法。

在我的JVM上,对单个slurp的内存使用增加了100MB,是文件大小的两倍。在解析数据时,我的内存使用量为1-2GB,具体取决于如何操作。如果我多次将文件打开并解析到同一个变量中,内存使用量会继续增加,最终会出现内存错误并导致程序失败。(我明白查看任务管理器不是查看内存泄漏的最佳方式,但事实是程序失败了,所以肯定存在某个地方的内存泄漏)

正确的文件打开方式是什么?我的最终用途是每天都会接收到一个新文件,我希望服务器应用程序每天打开文件并处理数据,而无需用完内存并重新启动服务器。

编辑:用Python pandas读取该文件将消耗大约100MB的内存,并且对文件的重复读取内存使用量不会持续增加。

非常感谢!

2 答案

+1

被选中
 
最佳答案

您需要将字段自定义解析为更小、更紧凑的格式。如果您存储一堆重复的字符串,请使用字符串规范化和重用重复字符串(共享引用)。通常的做法是使用字符串池在解析时共享引用。

我在此处发布了一个简短的演示项目此处,按两种方式处理这个任务
- 使用spork.util.table(我的旧表格处理库),它基于持久结构并使用我提到的技术
- 使用tech.ml.dataset,这是最近的一个新努力,利用TableSaw表格实现以快速、内存高效的结构(可变但COW实现以持久语义)。

这两个方案都可以轻松处理300mb的tsv文件(默认堆约2gb),尽管在实际应用中tech.ml.dataset的内存效率更高。

我还将一个版本放入此处,以复制您的测试输入(在缺乏信息的情况下,我在1/3处猜测),展示了使用spork、tech.ml(目前尝试解析日期时失败)和最后使用纯clojure函数手动执行的方法。如果您想要最小的内存占用量(和性能),则解析到原始数据类型是必需的。遗憾的是,Java集合通常是装箱的(除数组外),因此您需要一个像FastUtils或其他原始集合。Clojure原始向量有点帮助,但它们仍然因为数组(数组只位于叶子)的trie而产生参考开销。或者,如果您只需从文件构建一个类型同质的密集数组,您可以构造一个原始数组并用它填充。这将节省空间且满足机械性,如果您使用这些浮点数进行数值计算并且可以利用密集格式。

简而言之,- 朴素解析,尤其是当保留字符串或装箱对象引用时 - 将需要大量的堆,因为Java对象对内存的需求很大。正如Andy提到的那样,新版本的JVM可以通过压缩字符串(这实际上与字符串规范化做同样的事情,但在JVM级别上效果非常好)略微缓解这个问题。

注意:这仅适用于您必须“保留”数据的情况下......如果您可以对此进行迭代计算或进行其他流式计算,您可能可以绕过原始解析来自处理任意大小的数据集(例如,大于内存的大小)而不会耗尽堆,因为JVM会在您处理完毕后立即回收旧引用。

对于这类任务还有一些有用的库iota
以及最近通过libpython-clj包装的pandaspanthera

关于内存性能的说明:默认情况下,JVM不会将内存释放回系统,因此它将看起来在直到由-Xmx设置的限制(我认为默认是2GB)时“增长”,并且它将保持在那里(甚至在垃圾回收周期之后)。如果您想了解实际使用与保留的内存比例,则需要附加一个分析器并查看堆使用统计(如jvisualvm)。

非常感谢,非常有用的解释和最小示例!我会看看这些库。尝试从Python转移到Clojure以满足生产环境中的大部分数据处理需求(与探索性相对),这使得我更加珍视例如pandas等所有背后的工作!
是的,目前有一个名为[libpython-clj](https://github.com/cnuernber/libpython-clj)的整个项目,它正是针对你这样的人。它是功能性的,但开发者正在积极工作,以改善用户体验和兼容性,特别是以支持“只包装”库如pandas、numpy等。与tech.ml.dataset同一作者;你可能觉得它有帮助。如果你有兴趣,在[clojurians zulip](https://clojurians.zulipchat.com/#narrow/stream/215609-libpython-clj-dev)有一个开发频道。

在我看来,这对于Clojure来说一直是弱点,但现在正在得到纠正。很多人采用“映射序列”的方法 - 从API角度来看很棒,因为你可以直接利用seq/transducer库,实现非常自然的工怍流程。然而,数据表示非常臃肿,甚至对于中等大小的文件来说也根本不实用。我的很多工作都是因为遇到这些问题以及看到像R's datatable这样的库处理类似大小输入而开始的。但我认为tech.ml.dataset正在正确方向上(肯定是在空间和速度前沿),因为它是利用TableSaw(试图成为JVM上的pandas)的一些巧妙工作。
谢谢,已经知道libpython-clj,非常激动。你是对的,我的第一个方法就是映射序列:有5500列时,立即从内存角度崩溃了。已经看过spork,非常有趣!
最后的评论,以防对你有帮助:我在clojure.data.csv上做了一些测试,包括我展示的字符串池化以及tablesaw的实现(它使用bytemapdictionary,并提升到shortmap,具有一些有趣特性)。

总的来说,原始字符串池化和tablesaw的实现性能似乎相当。如果在解析CSV的过程中池化每个条目,大约可以实现4倍压缩。在我的测试数据集中,大约有2850779行,268MB的TSV文件,在i7处理器4GB堆栈大小的机器上,如果尝试将原始的data.csv序列实现为一个向量,堆栈爆炸并产生GC错误。使用字符串池化(尽管仍相当原始,但尝试缓存重复引用),我能够在45秒内构建向量,大约使用800MB的内存。调整池大小和边界可以提供微小的提升,但默认设置看起来相当不错。

https://gist.github.com/joinr/050a536b7ac01b50ae3dfa00cb7e5a74

使用流式处理和构建更有效的缓存结构可能更好,但如果你能承受0.8GB的堆栈使用量和大约一分钟的时间,那么对于你的用例来说可能还是可行的。显然,压缩效果会随着数据集的不同而变化(例如,如果你有非常稀疏的分类数据集,效果会更好)。
非常感谢Tom!
+2

在解析过程中,CSV文件中的每个字段都会在内存中成为一个独立的Clojure/Java字符串。在JDK 8中,每个Java字符串对象需要24字节,16字节用于数组对象,每字符2字节(即使是以ASCII格式存储,它们在内存中也是UTF-16编码,每字符2字节)。每个字段可能需要的40字节远大于每字符2字节,这取决于你的CSV文件有多少字段。如果你使用JDK 9或更高版本,如果字段只包含ASCII字符,紧凑字符串可以在内存中实现每字符1字节的内存优化,但这并不会减少每个字符串/字段40字节的大小。

谢谢,这帮助我理解内存使用!
...