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(当前尝试解析日期时失败)的方法,最后使用plain clojure函数手动处理。如果您想要最小的内存占用(以及性能),则解析到原语是必需的。但不幸的是,Java集合通常被装箱(除了数组之外),因此您需要像FastUtils或其他基于原语的集合。Clojure基于原语的向量有所帮助,但它们仍然因为数组的trie而承担着引用开销(数组仅位于叶子层)。或者,如果您只是从文件中构建具有相同类型的密集数组,则可以构造一个原语数组并填充它。这将是非常有效的空间利用,并且与机器非常融洽,尤其是当您使用这些浮点数进行数字运算并可以利用密集格式时。

总的来说——原始解析,尤其是当保留对字符串或装箱对象的引用时——将需要巨大的堆,因为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库进行工作流程。然而,数据表示严重膨胀,对于即使在普通大小的文件也不实际。我在遇到这些问题时,以及看到像R的datatable这样的库轻松处理类似大小的输入后,许多使用spork.util.table的工作都在进行中。我认为tech.ml.dataset正在走向正确的方向(在空间和速度前沿),因为它利用了TableSaw的一些巧妙工作(尝试在JVM上为pandas提供一个类似的产品)。
感谢您,我已经了解到libpython-clj了,非常令人兴奋。你是对的,我最初的方法是地图序列:有5500列,在内存方面立刻崩溃。
最后补充一下,如果您觉得有用:我试用了一下clojure.data.csv,以及我展示的字符串池和tablesaw的实现(它使用bytemapdictionary,并将其提升为shortmap,具有一些有趣特性)。

总的来说,天真字符串池和tablesaw的实现看起来表现差不多。如果您在解析CSV文件时池化每个条目,则可以获得约4倍压缩比。在我的测试数据集中,大约有2850779行,268MB的TSV文件,在一个4GB堆的i7上,如果我尝试将天真的data.csv序列实现为一个向量,那么堆就会破裂并出现GC错误。使用字符串池(仍然相当天真,但尝试缓存重复引用),我能在45秒内构建这个向量,大约使用800MB的内存。调整池大小和范围可以提供微小的提升,但默认值看起来相当不错。

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

流式传输和构建更高效的缓存结构可能更好,但如果您能承担0.8GB的堆使用量和大约一分钟的读取时间,那么这可能对于您的用例是可行的。显然,压缩效果会随着数据集而变化(例如,如果您的分类数据非常稀疏,效果会更好)。
非常感谢Tom!
+2

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

谢谢,这对了解内存使用大有帮助!
...