【问题标题】:Which Ruby methods to loop through numbers actually create the array and which do not?哪些循环数字的 Ruby 方法实际上创建了数组,哪些没有?
【发布时间】:2019-12-24 05:35:51
【问题描述】:

例如,给定n,可能是1000000100000000,找出来

0 ^ 1 ^ 2 ^ 3 ^ ... ^ n

有几种方法:

p (0..n).inject(:^)
p 0.upto(n).inject(:^)
p (n+1).times.inject(:^)

(在 Ruby 中还有其他常用方法吗?)在 Ruby 中循环遍历范围的常用方法中,哪些实际创建了一个数组,因此可能会导致大量内存使用? (作为要记住的一般规则)。

在 Mac 上,是否有办法在 Ruby 程序内部并使用操作系统来判断内存使用实际上是最小的(O(1) 空间)来验证没有创建实际的数组?

附:一种方法是使n 相当大,例如100000000,运行它,然后在另一个shell 中使用ps v 以查看内存处于低百分比(与使用(0..n).to_a.inject(:^) 运行程序相比),但是我不确定是否有更好的方法。

【问题讨论】:

  • 一般来说,Ruby 不会创建数组,除非它真的必须这样做,请参阅stackoverflow.com/q/6487747/479863(大约是 1.9.2,但行为变得更糟的可能性很小)。

标签: arrays ruby memory iterator


【解决方案1】:

这三个都没有创建数组。第一个创建Range,后两个创建Enumerator

证明(仅在 MRI 中,在 2.6.3p62 上测试):

# Does not create an array:
ruby -e 's = {}; GC.start; p "Before: #{ObjectSpace.count_objects(s)[:T_STRING]}"; ("AAAA".."ZZZZ").each { |x| p "During: #{ObjectSpace.count_objects(s)[:T_STRING]}" if x == "ZZZZ" }'
# => "Before: 8417"
# => "During: 16979"

# Creates an array:
ruby -e 's = {}; GC.start; p "Before: #{ObjectSpace.count_objects(s)[:T_STRING]}"; ("AAAA".."ZZZZ").to_a.each { |x| p "During: #{ObjectSpace.count_objects(s)[:T_STRING]}" if x == "ZZZZ" }'
# => "Before: 8417"
# => "During: 780125"

两者之间的唯一区别是#to_a 插入在#each 之前。确切的数字会随着运行而变化,但幅度应该明显不同。我使用字符串而不是整数,因为小整数不是作为对象实现的,也不能被 Objectspace::count_objects 计算。

编辑:但大整数是。 :)

ruby -e 'base = 4611686018427387904; s = {}; GC.start; p "Before: #{ObjectSpace.count_objects(s)[:T_BIGNUM]}"; base.upto(base + 10000).each { |x| p "During: #{ObjectSpace.count_objects(s)[:T_BIGNUM]}" if x == base }'
# => "Before: 3"
# => "During: 4"

ruby -e 'base = 4611686018427387904; s = {}; GC.start; p "Before: #{ObjectSpace.count_objects(s)[:T_BIGNUM]}"; base.upto(base + 10000).to_a.each { |x| p "During: #{ObjectSpace.count_objects(s)[:T_BIGNUM]}" if x == base }'
# => "Before: 3"
# => "During: 10005"

【讨论】:

  • 我想我们可以推断出来……尽管使用数字可能与常见用例更相关。我想知道为什么在第一种情况下对象数量会大幅增加。是不是因为创建了字符串对象但没有被垃圾收集。在第二种情况下,为什么这么多对象... 26**4 只有 456976
  • 不知道,也许 String#succ 创建了一个中间值或其他东西。但这是一个实现细节。我添加了一个带有整数的示例(它们只需要足够大以退出 fixnum 范围)。
  • 你怎么打电话给GC.start?不是自动的吗?
  • 这是自动的,但不可预测。我想从一个尽可能清晰的表格开始,因为这个例子就是要计算活动对象。
【解决方案2】:

连同@Amadan 的回答和another post on showing memory use 然后a even better one,以下可以显示循环内的内存使用情况:

def show_memory_used(s = "")
  puts "%.1fMB used %s" % [`ps -o rss= -p #{$$}`.to_f/1024, s]
end

show_memory_used("at the beginning")

n = 10000000

(0..n).each{|i| show_memory_used("during loop 1") if i == n}

0.upto(n){|i| show_memory_used("during loop 2") if i == n}

(n+1).times.to_a.each.each{|i| show_memory_used("during loop 3") if i == n}

样本输出:

6.0MB used at the beginning
6.0MB used during loop 1
6.0MB used during loop 2
6.0MB used during loop 3

将这些行中的任何一行更改为:

(0..n).to_a.each{|i| show_memory_used() if i == n}

实际创建一个数组,你可以看到使用的内存大大增加:

82.7MB used during loop 1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-22
    • 1970-01-01
    • 2011-11-19
    • 1970-01-01
    相关资源
    最近更新 更多