请在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

被选中
 
最佳答案

您需要将字段自定义解析为更小、更紧凑的类型。如果您存储了一堆重复的字符串,请使用字符串规范(String canonicalization)以复用重复的字符串(共享引用)。实现这一功能的典型方法是在解析时使用 字符串池(string pools) 来共享引用。

我在这里发布了一个简短的项目演示 (链接) ,它在这方面采用了两种方法。
- 使用 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 层面上与字符串规范执行相同的作用,但非常好)稍微缓解这个问题。

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

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

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

by
非常感谢,非常有帮助的解释和最小示例!我会研究一下库。由于我的生产环境大部分是数据处理的需要(与探索相反),所以这让我对比如 pandas 等背后的所有工作感到更加欣赏!
by
是的,目前有一个名为 [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)的一些巧妙工作。
by
感谢您,了解到libpython-clj,非常兴奋。而且您是对的,我最初的解决方案是地图序列:有5500列,立即在内存方面爆炸。看过spork,非常有趣的内容,!
最后评论,仅供参考:我用clojure.data.csv进行了一些测试,包括我演示的字符串池,以及tablesaw的实现(它使用bytemapdictionary提升到短地图,具有一些有趣的特征)。

总的来说,简单的字符串池和tablesaw的实现似乎性能相近。如果你在解析CSV时池化每个条目,那么你可以获得大约4倍的压缩比。在我的测试数据集中,约有2850779行,268MB的TSV文件,在i7中使用4GB堆时,如果我尝试将简单的data.csv序列转换为向量,则会突破堆并得到GC错误。使用字符串池(仍然相当简单,但尝试缓存重复引用),我能在45秒内构建这个向量,并且使用约800MB的内存。调整池大小和界限可以带来一些小的改进,但默认值似乎相当不错。

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

流式传输和构建更有效的缓存结构可能更好,但如果您能承受0.8GB的堆使用和大约一分钟的时间,也许它可以满足您的应用场景。很明显,压缩比将因数据集而异(例如,如果您有非常稀疏的类别数据,您可以做得更好)。
非常感谢您,Tom!

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

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