请在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文件,尽管默认堆(大约2GB),但在实际应用中tech.ml.dataset的记忆效率要显著更高。

我还将一个版本放入了测试中,以重复您的测试输入(在某种程度上,我猜测了多少空值,为1/3)。它展示了使用spork、tech.ml(目前解析日期时失败),以及最后使用纯Clojure函数手动处理的方法。如果您想要最小的内存足迹(以及性能),则解析为原语是必需的。不幸的是,Java 集合通常是装箱的(除了数组之外),所以您需要像FastUtils或另一个原始集合。Clojure原始向量有点帮助,但它们仍然因数组_certificate的引用而产生开销(数组只出现在叶子上)。或者,如果您只是从文件中构建一个同质类型的密集数组,您可以构建一个原始数组并填充它。这将非常节省空间且与机械性能相容,尤其是如果您正在使用这些浮点数进行数字处理并且可以利用密集格式。

底线是,- 原始解析,尤其是当保留对字符串或装箱对象的引用时 - 由于Java对象内存消耗大,将需要巨大的堆。正如Andy所提到的,较新的JVM可以通过压缩字符串(它有效地执行与字符串规范化相同的事情,但在JVM级别,真的很好)来稍微缓解这个问题。

注意:这仅适用于您必须“保留”数据的情况下……如果您可以对其进行折叠或以其他方式进行流计算,则可能可以在不影响堆的情况下使用原始解析处理任意数据集(例如,大于内存),因为JVM会在您处理完毕后立即回收旧引用。

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

关于内存性能的说明:默认情况下,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的一些巧妙的工作(TableSaw试图在JVM上提供与pandas类似的特性)。
感谢您,已经了解libpython-clj,并且非常兴奋。您说得对,我最先尝试的方法是将一系列的映射序列进行操作:有5500列,立即从内存的角度开始膨胀。已经查看了spork,非常有趣的内容!
最后一点评论,供您参考:我使用clojure.data.csv进行了一些测试,并结合了之前展示的字符串池和tablesaw的实现(它使用一个bytemapdictionary,该dictionary可以提升为包含一些有趣特性的shortmap)。

总的来说,直观的字符串池和tablesaw的实现似乎表现相近。如果您在解析CSV时池化每个条目,那么可以得到大约4倍的压缩。在我的测试数据集上,大约有2850779行,268MB的TSV文件,在i7处理器、4GB堆的情况下,如果尝试将直观的数据.csv序列转换为向量,则会突破堆的限制并得到GC错误。使用字符串池(仍然有些直观,但试图缓存重复引用),我能在45秒内构建向量,并且使用大约800MB的内存。调整池大小和边界可以带来小小的改进,但默认值似乎非常合理。

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

流式传输和构建一个更高效的缓存结构可能更好,但如果您能够承受0.8GB的堆使用量以及大约1分钟的读取时间,那么也许对您的用例来说是可以接受的。很明显,压缩率会随着数据集的不同而变化(例如,如果您有非常稀疏的分类数据集,您将表现得更好)。
非常感谢Tom!

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

谢谢,帮了我理清内存使用方面的困惑!
...