请在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
by
选择 by
 
最佳答案

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

我在这里发布了一个简短的演示项目这里,说明了两种方法
- 使用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包装器libpython-clj,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的一些巧妙的工作(尝试像pandas一样为JVM做)。
谢谢,我知道libpython-clj并且很有趣。你是对的,我是非常开始的方法是映射的序列:有5500列,内存泄漏是很严重的。我看了一下spork,非常有趣!
最后的要点,如果你觉得有用的话:我使用clojure.data.csv做了一些测试,结合了我展示的字符串池,以及tablesaw的实现(它使用bytemapdictionary,它可以转换到短映射,有一些有趣的特点)。

通常,简单的字符串池和tablesaw的实现性能看起来差不多。如果你在解析CSV时将每个条目池起来,那么你会得到大约4倍的压缩。在我的测试数据集中,大约有2850779行,268MB的TSV文件,在一个i7处理器,4GB堆栈的情况下,如果我将原始的数据.csv序列转换为向量,就会崩溃堆栈并得到GC错误。使用字符串池(虽然仍然有些简单,但尝试缓存重复引用),我能在45秒内构建向量,并使用大约800MB的内存。调整池大小和边界可以带来一些收益,但默认值看起来相当不错。

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

流式传输和构建一个更有效的缓存结构可能更好,但你如果要承受0.8GB的堆栈占用和大约一分钟的时间,这可能适合你的使用情况。显然,压缩量会因数据集而异(例如,如果你有非常稀疏的类别数据,你会做得更好)。
非常感谢Tom!
+2

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

by
谢谢,这帮助我了解了内存使用情况!
欢迎来到Clojure问答社区,在这里您可以向Clojure社区的成员提问并获得回答。

分类

...