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实现以支持持久语义)。

两种解决方案都可以轻松处理300MB的TSV文件,虽然tech.ml.dataset在实践中更节省内存。

我还将一个版本放在测试中,该测试复制了您的测试输入(在某种程度上,我猜测了空值的数量,为1/3)。它展示了使用spork,tech.ml(当前在尝试解析日期时失败)的方式,以及最后使用纯clojure函数手动进行的方式。如果您需要最小的内存占用(以及性能),则解析到原语是必需的。不幸的是,Java集合通常是装箱的(除了数组),因此您需要像FastUtils或另一个基于原语的集合。Clojure基于原语的向量有点帮助,但它们仍然因数组的trie而承担着引用开销(数组只位于叶子)。或者,如果您只是从文件构建一个相同类型的密集数组,可以构造一个原语数组并将其填充。这将节省空间,并且在结构上相容,特别是如果您使用这些浮点数进行数值计算并能利用密集格式。

总之,原始解析,特别是保留对字符串或装箱对象的引用,将需要庞大的堆,因为Java对象很耗内存。正如Andy提到的,新的JVM可以通过压缩字符串(这与字符串规范化的效果相同,但在JVM级别真正很好)在一定程度上减轻这种负担。

注意:这仅适用于您需要“保留”数据的情况......如果您可以遍历它或以其他方式流式计算,则可能可以通过原始解析处理任意数据集(例如超过内存的大小)而不爆堆,因为JVM会在您处理完引用后立即进行垃圾回收。

还有一些库可用于此类任务iota
以及最近的pandas包装器via libpython-clj,panthera

关于内存性能的笔记:默认情况下,Java虚拟机(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库,且工作流程非常自然。然而,数据表示过于膨胀,连中等的文件都处理不过来。我与spork.util.table合作的大部分工作都是在遇到这些问题的同时,以及看到像R的datatable这样的库处理类似大小的输入。我认为tech.ml.dataset走在了正确的道路上(无疑在空间和速度的前沿),因为它利用了TableSaw的一些巧妙的工作(它试图在JVM上为pandas提供一个替代品)。
感谢,我已了解libpython-clj,并且非常兴奋。你是对的,我第一次的方法就是映射序列:有5500列,从内存角度看很快就崩溃了。研究了spork,非常有趣的内容!
by
最后的评论,仅供参考:我用 clojure.data.csv 进行了一些测试,以及我之前展示的字符串池和 tablesaw 的实现(它使用一个扩展为短映射的 bytemapdictionary,具有一些有趣的属性)。

总的来说,简单的字符串池和 tablesaw 的实现性能似乎差不多。如果在解析 CSV 时将每个条目放在一起,那么可以得到大约 4 倍的压缩率。在我的测试数据集中,大约有 2850779 行,268 MB 的 TSV 文件,在一个 4GB 堆的 i7 上,当我尝试将简单的数据.csv 序列转化为一个向量时,堆爆了,并且得到一个 GC 错误。使用字符串池(仍然有些简单,但尝试缓存重复引用),我能在 45 秒内构建向量,大约使用 800 MB 的内存。调整池大小和范围可以提供一些微小的增益,但默认值看起来相当不错。

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

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

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

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