请在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,是文件大小的2倍。在解析数据时,取决于如何处理,内存使用量会上升到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原语支持向量有所帮助,但它们仍然因数组的trie(数组只位于叶子)而产生引用开销。或者,如果您只是从文件中构建一个由同质类型构成的密集数组,可以构造一个原始数组并填充它。这将节省空间并机械上一致,尤其是如果您使用这些浮点数进行数值运算并可以利用密集格式。

总之,- 原始解析,尤其是保留字符串或装箱对象的引用 - 由于Java对象占用内存较多,需要巨大的堆。

注意:这仅适用于您必须“保留”数据的情况......如果您可以对其进行reduce操作或其他流式计算,则可以尽可能避免使用原始解析进行任意大数据集(例如大于内存的)而不破坏堆,因为JVM将在您处理完后立即回收旧引用。

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

内存性能提示:JVM默认不会将内存释放回系统,因此它的内存使用量看起来会“增长”到由-Xmx参数设置的极限(默认为2GB左右),并且似乎会停留在那里(即使在GC周期之后)。如果您想了解实际使用量和预留量,您需要附加一个性能分析器并查看堆使用统计信息(例如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,很有趣!
by
最后评论,以防对你有用:我用 clojure.data.csv 进行了一些测试,包括我展示的字符串池以及 tablesaw 的实现(它使用 bytemapdictionary,并将其提升到具有一些有趣属性的 shortmap)。

总的来说,简单的字符串池和 tablesaw 的实现看起来性能差不多。如果你在解析 CSV 时池化每个条目,那么你会得到大约 4 倍的压缩。在我的测试数据集中,大约有 2850779 行,268 MB 的 TSV 文件,在我的 i7 处理器上,4GB 堆的情况下,如果我将简单数据.csv 序列化为 vector,那么堆就爆了,会得到垃圾收集错误。使用字符串池(仍然相当简单,但尝试缓存重复引用),我能在 45 秒内构建 vector,并使用大约 800MB 的内存。调整池大小和边界可以提供一定的提升,但默认设置似乎相当不错。

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

流处理并构建一个更有效的缓存结构可能更好,但如果你能承受 0.8GB 的堆使用和大约 1 分钟的读取时间,可能对您的用例来说是可行的。显然,压缩将随着数据集的不同而变化(例如,如果您有一个类别数据非常稀疏的集合,您会做得更好)。
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 字节。

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