【问题标题】:Why does my Ruby script slow down over time?为什么我的 Ruby 脚本会随着时间的推移变慢?
【发布时间】:2026-01-13 06:25:01
【问题描述】:

我有一个 2.6 GB 的文本文件,其中包含一个数据库表的转储,我正在尝试将其拉入一个逻辑结构,以便所有字段都是唯一的。我用来执行此操作的代码在这里:

class Targetfile
  include Enumerable

  attr_accessor :inputfile, :headers, :input_array

  def initialize(file)
    @input_array = false
    @inputfile = File.open(file, 'r')
    @x = @inputfile.each.count
  end

  def get_headers
    @y = 1
    @inputfile.rewind
    @input_array = Array.new
    @headers = @inputfile.first.chomp.split(/\t/)
    @inputfile.each do |line|
      print "\n#{@y} / #{@x}"
      @y+=1
      self.assign_row(line)
    end
  end

  def assign_row(line)
    row_array = line.chomp.encode!('UTF-8', 'UTF-8', :invalid => :replace).split(/\t/)
    @input_array << Hash[ @headers.zip(row_array) ]
  end

  def send_build
    @input_array || self.get_headers
  end

  def each
    self.send_build.each {|row| yield row}
  end

end

类初始化成功,剩下一个 Targetfile 类对象。

问题在于,当我随后调用 get_headers 方法(将文件转换为哈希数组)时,它立即开始减速。

直到项目编号 80,000 左右,我的眼睛才注意到这一点,但随后很明显,文件的每 3-4,000 行,就会发生某种暂停。这种停顿,每次发生时,都需要稍长一点的时间,直到第 100 万行,它需要的时间超过 30 秒。

出于实际目的,我可以将文件切碎以避免此问题,然后将结果列表和唯一的 -that- 组合起来以获得我的最终输出。

但是,从好奇心的角度来看,我并不满意。

谁能告诉我为什么会发生这种停顿,为什么会变长,以及是否有任何方法可以优雅地避免它?真的,我只是想知道它是什么以及它为什么会发生,因为现在我已经注意到了,我在我运行的许多其他 Ruby 脚本中都看到了它,无论是在这台计算机上还是在其他计算机上。

【问题讨论】:

  • 你能在一个更小的文件上试试这个吗?它将帮助您确定问题是代码还是文件大小
  • 使用@x = %x(wc -l path/to/file).to_i 代替@x = @inputfile.each.count。 wc 非常快,专为此而设计。
  • 您能否在运行此脚本时监控 ruby​​ 需要多少 RAM?
  • 也许您的系统内存不足,并且在加载足够的行后开始使用页面文件?检查您的进程内存消耗。
  • 啊。内存消耗在脚本执行过程中不断增加,直到耗尽所有可用内存并交换启动。减速发生在计算机的 RAM 已满之前,但在填满后会变得更糟。

标签: ruby performance file-io class-method


【解决方案1】:

我建议在 DBM 中执行此操作,而不是 Ruby 或任何其他语言。 DBM 可以非常快速地告诉您字段的唯一值,尤其是在它已经被索引的情况下。

试图用任何语言来做这件事就是在为通用计算设计的东西中复制数据库的基本功能。

相反,将 Ruby 与 Sequel 或 Active Record 之类的 ORM 结合使用,并向数据库发出查询并让它返回您想知道的内容。不要遍历每一行,这太疯狂了,要求它给你独特的价值,然后从那里开始。

我不会责怪 Ruby,因为在相同主机和 RAM 的情况下,任何其他语言都会出现同样的问题。 C/C++ 可能会通过生成更紧凑的代码来延迟不可避免的事情,但是您的开发时间会大大减慢,尤其是当您学习像 C 这样的未知语言时。并且意外错误的风险会上升,因为您必须做更多的内务和防御工作比您在 Ruby、Python 或 Perl 中进行的编程。

将每个工具用于其设计用途,您就会取得领先。

查看您的代码,您可能会通过不尝试将每一行都保留在内存中来提高完成完整运行的机会。您说您正在尝试确定唯一性,因此只保留您感兴趣的唯一列值,您可以使用 Ruby 的 Set 类轻松完成。您可以将要确定唯一性的每个事物的值抛出,遍历文件,Set 只会保留唯一值。

【讨论】:

  • 数据库可以很好地管理大型数据集,这就是它们的用途。
  • (是的,我最终通过将它们加载到 mysql 数据库并按照您的建议进行处理。实际上,我刚刚回到 * 打算发布“durp,错误的工具开始工作.")
  • 这就是我们学习的方式。我们将 Ruby 用于一些让我们的管理层感到惊讶的重量级事情,因为当我们使用小型 VM 切片时,我们可以在专用硬件上运行非常昂贵的软件。这一切都在于抓住正确的工具并让他们做自己擅长的事情。
【解决方案2】:

这就是臭名昭著的垃圾收集器——Ruby 的内存管理机制。

注意:值得一提的是,Ruby,至少 MRI,不是一种高性能语言。

只要内存开始用完,垃圾收集器就会运行。垃圾收集器暂停程序的执行以释放任何无法再访问的对象。垃圾收集器仅在内存开始用完时运行。这就是为什么您会定期看到它。

除了编写内存效率更高的代码,或者用可以更好/手动内存管理的语言重写之外,您无能为力。

另外,您的操作系统可能正在分页。您是否有足够的物理内存来执行此类任务?

【讨论】:

  • 我想我不能。嗯,真可惜。我现在只需要把这个傻瓜分成几块,然后找一门 C/C++ 课程。感谢您的提示。
  • @MaxwellEvans:当然。 Ruby 不适合严肃的数据处理。你有多少内存?什么操作系统?
  • 运行 Linux (ubuntu),8 gigs 物理内存
  • @MaxwellEvans:你有足够的内存。
【解决方案3】:

您正在使用标头作为散列的键。它们是字符串,并且散列重复的字符串键。那是很多不必要的字符串。尝试将它们转换为符号是否可以加快速度:

@headers = @headers.map{|header| header.to_sym}

【讨论】:

    【解决方案4】:

    这是垃圾收集器。您可以通过在程序中放入 GC.start 来强制进行垃圾收集。让它定期运行。 我必须为我编写的守护进程做同样的事情。它运作良好。 http://ruby-doc.org/core-1.9.3/GC.html

    【讨论】:

      最近更新 更多