Clojure:如何从内存中删除对象?

Posted

技术标签:

【中文标题】Clojure:如何从内存中删除对象?【英文标题】:Clojure : how can I remove an object from memory? 【发布时间】:2016-04-25 10:08:40 【问题描述】:

我刚刚完成了一个程序,该程序基本上收集数据并计算几个表(它们通常因过滤器或聚合级别而异(我写了一个 SELECT + GROUP BY 类似 Clojure 函数),我最终用它来计算一个汇总表。这个汇总表稍后使用,只有 30 000 行长。

为了让你明白,这是我必须启动的命令:

(def summary-pages (merge-summary-pages (merge-pages-stock (compute-stock) (compute-pages) (compute-xx-prices) (compute-g) (compute-brands) (compute-prices) (compute-sales) (compute-sales-month))
                                        (merge-full-pages-per-page (merge-full-pages (merge-pages-stock (compute-stock) (compute-pages) (compute-xx-prices) (compute-g) (compute-brands) (compute-prices) (compute-sales) (compute-sales-month)) (merge-pages-stock-excluded (compute-pages) (compute-stock) (compute-g) (compute-brands) (compute-prices) (compute-sales) (compute-sales-month))))
                                        (merge-pages-stock-per-page (merge-pages-stock (compute-stock) (compute-pages) (compute-xx-prices) (compute-g) (compute-brands) (compute-prices) (compute-sales) (compute-sales-month)))
                                        (merge-affectations-count (compute-affectations)))) 

如你所见,我多次调用相同的数据(实际上(compute-pages) 调用了compute-affectations

这可行,但问题是 compute-pages 尤其是 compute-affectations 在 google BQ(1500 万行)和 microsoft sql server(4500 万行)上是相当大的查询。

问题是查询它们需要时间 4-5 次,我也害怕破坏数据库。 另一个问题是我必须计算所有compute-affectations,因为它来自sql server,而我的左连接使用group-by。

我尝试使用 def 拆分作业,但出现 GC 开销错误。 因为经过一些计算我可以清理affectations,所以我尝试了

(def affectations nil)

...没有任何改变,我在微软命令面板中没有看到可用内存。

有没有办法清理内存?

在 Python 中,我的程序完全没有问题(实际上内存使用率最多为 80%),但在这里,堆空间为 13gb,当我将 def 用于任何子数据时,我总是遇到问题。 我有 16gb 的内存,所以我无法增加堆空间,而且我觉得奇怪的是,这样的“小”数据占用了这么多内存。

我用csv计算了测试数据,基础数据只有3.3gb...

编辑

工作代码(部分):

            (let [_ (init-config! path)
                 _ (write-affectations)
                 _ (write-pages)
                 _ (spit "prepared-pages.edn" (prn-str (prepare-pages (read-pages))) :append :true)
                 _ (write-stock)
                 _ (write-prices)
                 _ (write-xx-prices)
                 _ (write-brands)
                 _ (write-g)
                 _ (write-vehicles)
                 _ (write-sales)
                 _ (write-sales-month)
                 _ (System/gc)
                stock (read-stock)
                affectations (read-affectations)
                pages (read-pages)
                prepared-pages (prepare-pages pages)
                xx-prices (read-xx-prices)
                g (read-g)
                brands (read-brands)
                prices (read-prices)
                sales (read-sales)
                sales-month (read-sales-month)
                pages-stock (merge-pages-stock stock prepared-pages xx-prices g brands prices sales sales-month)
                pages-stock-excluded (merge-pages-stock-excluded prepared-pages stock g brands prices sales sales-month)
                full-pages-per-page (-> (merge-full-pages pages-stock pages-stock-excluded)
                                        (merge-full-pages-per-page))
                pages-stock-per-page (merge-pages-stock-per-page pages-stock)
                affectations-count (merge-affectations-count affectations)
                summary-pages (doall (merge-summary-pages pages-stock full-pages-per-page pages-stock-per-page affectations-count))
                _ (System/gc)
                _ (io/delete-file "affectations.edn")
                _ (io/delete-file "pages.edn")
                _ (io/delete-file "prepared-pages.edn")
                _ (io/delete-file "stock.edn")
                _ (io/delete-file "prices.edn")
                _ (io/delete-file "xx-prices.edn")
                _ (io/delete-file "brands.edn")
                _ (io/delete-file "g.edn")
                _ (io/delete-file "vehicles.edn")
                _ (io/delete-file "sales.edn")
                _ (io/delete-file "sales-month.edn")

我将查询的内容写在 HDD(.edn 文件)上,然后我懒惰地读取它并将其传递给函数。

谢谢!

【问题讨论】:

如果所有对compute-pagescompute-affectations 的调用每次都返回相同的数据,为什么不使用let 你好,谢谢我也试过了,但事实上我所有的数据都在一个很大的地方,因为我在 -main 部分。事实上,即使我描述的命令也不适用于 sql server 和 BQ。我发现了一个愚蠢的调整,但它正在工作:首先查询这两个特定的表,立即将其按块写入 HDD (CSV),然后将其传递给 (compute-summary-pages)。由于我根本不是专家级程序员,所以我对发生的事情一无所知:也许字符串数据消耗了太多内存(BQ)和/或 SQL 查询不是懒惰的?? 顺便说一句,这是一类问题,一个好的内存分析器在解决方面将是无价的。它需要花钱(除非您将它专门用于 OSS 工作并申请免费许可证),但 YourKit 做得很好。 【参考方案1】:

在不确切知道这些功能的情况下,很难检查以下情况:

抓住一个巨大的懒惰序列的头部 惰性错误(代码在读取惰性结果序列之前关闭数据库连接) 在内存中保存太多相同内容的副本。

这最后一个显然是您问题的一部分(或全部)。我对您的示例进行了重复数据删除,因此每个值只计算一次。

(def summary-pages
  (let [stock (compute-stock)
        pages (compute-pages)
        prices (compute-prices)
        brands (compute-brands)
        sales-month (compute-sales-month)
        g (compute-g)
        sales (compute-sales)
        xx-prices (compute-xx-prices)
        pages-stock (merge-pages-stock stock pages xx-prices g brands prices sales sales-month)
        pages-stock-excluded (merge-pages-stock-excluded pages stock g brands prices sales sales-month)
        full-pages-per-page (merge-full-pages-per-page (merge-full-pages pages-stock pages-stock-excluded))
        pages-stock-per-page (merge-pages-stock-per-page pages-stock)
        affectations-count (merge-affectations-count (compute-affectations))]
       (merge-summary-pages pages-stock 
                            full-pages-per-page
                            pages-stock-per-page
                            affectations-count))) 

下一步是注释掉除第一个之外的所有内容,验证它是否在合理的时间内正确运行。然后取消注释下一个并重复。


由于您的数据很大,并且您希望您的代码一次使用惰性序列处理它的一个窗口,所以我想向您介绍“本地绑定”和“本地清除”如何在 Clojure 中工作。

下面是一个“应该”通过持有十亿个整数序列的头部而导致内存不足异常的序列示例:

user> (time
       (let [huge-sequence (range 1e9)]
         (last huge-sequence)))
"Elapsed time: 49406.048091 msecs"
999999999

但它没有?为什么?

它一定是以某种方式发现它实际上不需要存储大序列的头部,因为之后没有人会使用它!所以让我们通过将示例更改为它必须保持头部并查看它是否仍然有效来测试它

user> (time
       (let [huge-sequence (range 1e9)]
         (last huge-sequence)
         (first huge-sequence)))

这一次我的 CPU 风扇启动,所有 8 个处理器立即旋转到 100%。五分钟后我开始不耐烦了,所以我会出去吃午饭,看看它是否在我吃饭的时候结束。到目前为止,我们的假设看起来是准确的。

因此,当您在一个大的 let 块中定义一堆序列,然后进行一个函数调用,多次使用它们并且在调用之后从不接触它们时,序列的头部通常可以在函数调用时间,它将“神奇地”正常工作。这就是为什么我建议您从上到下一次测试这些。

几乎在 Clojure 中没有理由重复定义相同的数据,我们在这里非常重视引用透明性。


更新:测试最终耗尽了内存,尽管时间调用被抛出的异常绕过了,所以我不知道它实际花了多长时间:
user> (time
       (let [huge-sequence (range 1e9)]
         (last huge-sequence)
         (first huge-sequence)))
OutOfMemoryError Java heap space  [trace missing]

【讨论】:

我也会尝试,但是如果我很好地理解它是如何工作的,当我在一个 let 中绑定一个惰性阅读器时,如果我计算一次所有的序列,它将保留在内存中。当我进行这个大计算(我的代码)时,一旦计算了超级数据(比如说每页整页),我会看到内存大幅下降,因为中间数据被清理了。这就是为什么它有效而不是绑定所有。我几乎拥有大量内存,因为当我关闭所有应用程序时,我没有真正的内存问题。我也在 edn 中编写了所有内容,这样我就可以在没有下载时间的情况下懒惰地访问数据。 您的评论涉及很多内容,因此我将在答案中添加基础知识,如果您愿意,我们可以稍后再谈具体内容。 感谢您的回答,我尝试了您的解决方案,它也可以正常工作。它可能会更好,因为似乎计算时间更快,但在某些时候我仍然处于最大内存。如果它有效并且代码现在更好,那不是问题。我认为真正有问题的是 MS SQL 查询,将其写入 HDD 可以让我用 line-seq 懒惰地读取它。我的印象是 Clojure 将最后一个对象保存在内存中,即使它们没有被绑定,内存清理仍然发生得很好!我用示例代码编辑了我的答案 感谢您将完成的答案添加到其他人以后可以从中学习。

以上是关于Clojure:如何从内存中删除对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用clojure.spec生成相互关联的args?

预售┃Scala与Clojure函数式编程模式:Java虚拟机高效编程

Clojure 懒惰地从文件中读取随机行

深入理解 Clojure Persistent Vectors 实现

如何使用 clojure.test 将值从夹具传递到测试?

理解Clojure STM 软件事务性内存