Clojure2024状态调查中分享你的想法!

欢迎!请查看关于页面,了解更多关于如何使用此服务的详细信息。

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
by
chosen by
 
最佳答案

您需要将字段自定义解析为更小、更紧凑的类型。如果您正在存储许多重复的字符串,请使用字符串规范化和重用重复字符串(共享引用)。典型的做法是使用在解析时共享引用。

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

这两种解决方案都可以很容易地处理大约300MB的TSV文件(默认堆大小约为2GB),尽管在实际情况中tech.ml.dataset的内存效率要高得多。

我还在放了一个版本,它可以复制您的测试输入(在一定程度上是猜测的,大约是1/3)。它显示了如何使用spork、tech.ml(目前尝试解析日期时失败),最后使用纯clojure函数手动处理。如果您希望最小化内存占用(和性能),则解析到原始数据类型是必要的。不幸的是,Java集合通常是装箱的(除数组外),因此您需要像FastUtils这样的工具或另一个原始数据类型集合。Clojure原始数据类型向量有点帮助,但它们仍然因为数组的字典(数组仅存在于叶子中)而承担引用开销。或者是如果您只是从文件中构建同质类型的密集数组,您可以构造原始数组并填充它。这将节省空间并且机械上兼容,尤其是在您使用浮点数进行数值计算并可以利用密集格式时。

底线是 —— 天真的解析,特别是当保留对字符串或装箱对象的引用时 —— 将需要巨大的堆,因为Java对象消耗内存。

注意:这仅适用于必须“保留”数据的情况……如果您可以对其进行折叠或其他流式计算,则可能无需爆堆即可使用天真解析任意数据集(例如,大于内存),因为JVM会在处理旧引用之后立即进行垃圾回收。

还有一些库可以用于这项任务
以及最近通过libpython-clj的pandas包装器

关于内存性能的注意事项:JVM 默认不会将内存释放回系统,所以它的内存使用量会“增长”到通过 -Xmx(默认我认为是 2GB)设置的极限,并且它似乎会停留在那里(即使经过垃圾回收周期)。如果您想了解实际使用的情况与预留的情况,您需要附加一个剖析器并查看堆使用情况(例如 jvisualvm)。

非常感谢,解释得很清楚,示例也很简单!我会查看这些库。我在尝试将生产中的大部分数据处理需求从 Python 迁移到 Clojure(与探索不同),这让我更加珍惜像 pandas 这样的工作!
是的,目前有一个名为 [libpython-clj](https://github.com/cnuernber/libpython-clj) 的整个项目,旨在满足像您这样的人。它目前是功能性的,但开发者们正在积极工作以改善用户体验和兼容性,特别是支持“仅包装”库(如 pandas、numpy 等)。它与 tech.ml.dataset 同作者,您可能会觉得它有帮助。如果您感兴趣,还可以在 clojurians zulip 的 [libpython-clj 开发频道](https://clojurians.zulipchat.com/#narrow/stream/215609-libpython-clj-dev) 关注。

在我看来,这曾是 Clojure 的一大弱点,但现在正在得到解决。许多人采取了一种“映射序列”的方法 - 从 API 视角来看,这很好,因为在非常自然的作业流程中可以仅利用所有 seq/transducer 库。然而,数据表示过于膨胀,对于即使是中等大小的文件来说也不切实际。我研究 spork.util.table 的许多工作都是在遇到这些问题的同时,以及看到像 R 的 datatable 这样的库能够处理类似大小的输入后产生的。不过,我认为 tech.ml.dataset 路线是正确的(无疑在空间和速度的前端),因为它利用了一些 TableSaw 的巧妙工作(该工作试图给 JVM 版本的 pandas 带来类似功能)。
谢谢,我已经熟悉了libpython-clj,这非常令人兴奋。你的说法没错,我最初的方法是将序列作为映射:有5500列,会立即因为内存问题而崩溃。我已经查看过spork,非常有趣的内容,!
评论者:
最后一点评论,供参考:我与clojure.data.csv做了一些测试,结合了之前展示的字符串池技术以及tablesaw的实现(它使用了具有某些有趣属性的bytemapdictionary)。

总的来说,直接字符串池化和tablesaw的实现表现大致相同。如果您在解析CSV时池化每个条目,则可以得到大约4倍的压缩。在我的测试数据集中,大约有2850779行,268MB的TSV文件,在一个4GB堆的i7上,我想将直接数据.csv序列实现到一个向量化中时遇到了堆溢出和GC错误。使用字符串池化(仍然相当简单,但尝试缓存重复引用),我能在45秒内构建这个向量,并使用大约800MB的内存。调整池大小和界限可以带来一些小的好处,但默认值似乎相当不错。

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

流式传输并构建一个更有效的缓存结构可能更好,但如果您可以承受0.8GB的堆使用量和大约1分钟的读取时间,那么它可能适用于您的用例。显然,压缩率会随着数据集而变化(例如,如果您有稀疏的类别数据集,您会做得更好)。
评论者:
非常感谢Tom!
+2
作者:

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

by
感谢,这帮助我了解了内存使用情况!
...