2024 Clojure调查问卷中分享您的想法!

欢迎!有关此如何操作的更多信息,请参阅关于页面。

0
Clojure
编辑

你好,

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

我已经尝试了从

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

到通过使用 line-seq 和手动解析字符串为数手动一些更复杂的选择。

使用单个 slurp JVM的使用量会增加到100MB,是文件大小的两倍。在解析数据时,根据处理方式的不同,我会增加到1-2GB。如果打开和解析文件多次到同一个变量中,内存使用量会持续增加,最终导致内存错误而程序失败。(我知道查看任务管理器并不是查看内存泄漏的最佳方式,但事实上程序失败了,所以确实存在泄漏)

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

编辑:与此文件相比,使用Python pandas读取将消耗大约100MB的内存,后续重新读取文件不会持续增加内存使用量。

非常感谢!

2 个答案

+1

已选择
 
最佳答案

您需要自定义将字段解析为更小的、更紧凑的类型。如果您正在存储大量的重复字符串,请使用字符串规范化的方法重新使用重复的字符串(共享引用)。通常的做法是在解析时使用字符串池来共享引用。

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

这两种解决方案都能轻松处理默认堆(约 2gb)下的 300mb tsv,尽管 tech.ml.dataset 在实际应用中显著更节省内存。

我还将一个版本放在测试中,该版本可以复制您的测试输入(在空项的数量上,我猜测了 1/3)。它展示了使用 spork、tech.ml(当前在尝试解析日期时失败)以及最后使用纯 clojure 函数手动处理的方法。如果您希望最小化内存占用(以及性能),那么解析为原始数据类型是必需的。不幸的是,Java 集合通常是装箱的(除了数组之外),因此您需要类似 FastUtils 的东西或其他原始数据支持的集合。Clojure 的原始数据支持向量有些帮助,但它们仍然因为数组的 trie 而产生引用开销(数组只在树叶)。或者,如果您只是从文件中构建同构类型的密集数组,您可以构建一个原始数组并填充它。这将空间高效,技术上兼容,尤其是如果您使用这些浮点数进行数值运算并且可以利用密集格式。

结论是,直观解析,尤其是当保留字符串或装箱对象的引用时,将需要庞大的堆,因为 Java 对象很耗内存。正如 Andy 所提到的,较新的 JVM 可以通过压缩字符串(这在 JVM 级别真的很好)在一定程度上缓解这种情况,与字符串规范化有相同的效果。

注意:这仅适用于您必须“保留”数据的情况……如果您可以对其降维或以其他方式进行流式计算,那么对于任意数据集(例如,大于内存的数据集)的直观解析可能不足以破坏堆,因为 JVM 将在您处理完旧引用后立即进行垃圾回收。

还有一些其他用于此类任务的有用库 iota
以及最近的通过 libpython-clj 的 pandas 包装器 panthera

关于内存性能的说明:JVM默认不会将内存释放回系统,因此它看起来会“增长”到由-Xmx指定的限制(我认为默认是2GB),并且它会停留在那里(即使在GC周期之后)。如果您想了解实际使用的是什么与预留的是什么,您需要连接一个分析器并查看堆使用统计信息(如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的datatable能够轻松处理类似大小的输入而开始的。但是,我认为tech.ml.dataset正在正确的道路上(显然在空间和速度前沿),因为它利用了TableSaw的一些巧妙工作(TableSaw试图成为JVM上的pandas)。
谢谢提醒,我已经了解了libpython-clj,非常有趣。您说得对,我最早的方法是使用一系列的map:有5500列,(memory方面)很快就爆发了。看过spork,非常有趣的内容,!
最后一条评论,以防有用:我使用clojure.data.csv进行了一些测试,结合我所展示的字符串池化和tablesaw的实现(它使用bytemapdictionary,并将其提升到shortmap,具有一些有趣属性)。

一般来说,天真字符串池化和tablesaw的实现似乎表现相似。如果你在解析csv时池化每一条记录,那么你将得到大约4倍压缩。在我的测试数据集中,大约是2850779行,一个268MB的tsv文件,在i7处理器、4GB堆栈的情况下,我尝试把天真数据.csv的seq实现到一个向量中,导致堆栈爆满,并且发生GC错误。使用字符串池化(仍然有点天真,但在尝试缓存重复引用),我能在45秒内构建向量,使用大约800MB的内存。调整池大小和边界可以提供一些小的改进,但默认设置似乎相当不错。

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

流和构建更高效的缓存结构可能更好,但如果您能承受0.8GB的堆栈使用并花费大约一分钟的时间阅读,那么这可能适合您的使用情况。显然,压缩效果会因数据集而异(例如,如果您有非常稀疏的类别数据集,您会做得更好)。
非常感谢Tom!
+2

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

谢谢,这帮助我理解了内存占用问题!
...