Warning: file_get_contents(/data/phpspider/zhask/data//catemap/5/ruby/21.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Ruby 为什么这两个较小的循环比包含相同指令的单个循环慢得多?_Ruby_Benchmarking - Fatal编程技术网

Ruby 为什么这两个较小的循环比包含相同指令的单个循环慢得多?

Ruby 为什么这两个较小的循环比包含相同指令的单个循环慢得多?,ruby,benchmarking,Ruby,Benchmarking,我有以下性能基准脚本,用于测试一个较大循环操作与两个较小循环之间的差异: Ruby v2.3.3p222 MacOS Catalina v10.15.3 Processor: 2.6GHz 6-Core Intel Core i7 我的假设是,这两个方法将在大约相同的时间内执行,因为(我认为)它们都执行相同数量的指令:较大的循环执行2*10000000个操作,而两个较小的循环中的每个循环执行1*10000000个操作 然而,这似乎不是我观察到的。当我运行脚本时,我得到以下输出: require

我有以下性能基准脚本,用于测试一个较大循环操作与两个较小循环之间的差异:

Ruby v2.3.3p222
MacOS Catalina v10.15.3
Processor: 2.6GHz 6-Core Intel Core i7
我的假设是,这两个方法将在大约相同的时间内执行,因为(我认为)它们都执行相同数量的指令:较大的循环执行2*10000000个操作,而两个较小的循环中的每个循环执行1*10000000个操作

然而,这似乎不是我观察到的。当我运行脚本时,我得到以下输出:

require 'benchmark'

N = 10_000_000

def one_loop
  N.times do
    foo = 1+1
    bar = 2+2
  end
end

def two_loops
  N.times do
    foo = 1+1
  end

  N.times do
    bar = 2+2
  end
end


Benchmark.bmbm do |performance|
  performance.report("two smaller loops") { two_loops }
  performance.report("one large loop") { one_loop }
end
这真的令人失望,因为我希望说服我的团队,通过将1个大的代码循环分解成几个更简洁的循环,每个循环都做了一件事并且做得很好,我们不会看到任何性能降低

我认为这可能是由于生成报告的顺序造成的,但当我颠倒对
performance.report
的两个调用的顺序时,我得到了同样令人失望的结果:

Rehearsal -----------------------------------------------------
two smaller loops   0.840000   0.000000   0.840000 (  0.838101)
one large loop      0.500000   0.010000   0.510000 (  0.506283)
-------------------------------------------- total: 1.350000sec

                        user     system      total        real
two smaller loops   0.850000   0.000000   0.850000 (  0.863052)
one large loop      0.500000   0.000000   0.500000 (  0.494525)

我错过什么了吗?两个较小的循环真的比单个较大的循环做了更多的工作吗?还是我不知何故以误导或不准确的方式构建了我的基准脚本?

这是1000万次迭代,在每次迭代中进行两次计算,总共3000万次我们称之为操作:

Rehearsal -----------------------------------------------------
one large loop      0.500000   0.010000   0.510000 (  0.508246)
two smaller loops   0.850000   0.000000   0.850000 (  0.852467)
-------------------------------------------- total: 1.360000sec

                        user     system      total        real
one large loop      0.490000   0.000000   0.490000 (  0.496130)
two smaller loops   0.830000   0.000000   0.830000 (  0.831476)
  N.times do
    foo = 1+1
    bar = 2+2
  end
这是2000万次迭代,在每次迭代中都会进行一次计算,总共有4000万次我们称之为操作:

Rehearsal -----------------------------------------------------
one large loop      0.500000   0.010000   0.510000 (  0.508246)
two smaller loops   0.850000   0.000000   0.850000 (  0.852467)
-------------------------------------------- total: 1.360000sec

                        user     system      total        real
one large loop      0.490000   0.000000   0.490000 (  0.496130)
two smaller loops   0.830000   0.000000   0.830000 (  0.831476)
  N.times do
    foo = 1+1
    bar = 2+2
  end

30<40,因此第一个示例更快。

这是1000万次迭代,在每次迭代中进行两次计算,总共3000万次我们称之为操作:

Rehearsal -----------------------------------------------------
one large loop      0.500000   0.010000   0.510000 (  0.508246)
two smaller loops   0.850000   0.000000   0.850000 (  0.852467)
-------------------------------------------- total: 1.360000sec

                        user     system      total        real
one large loop      0.490000   0.000000   0.490000 (  0.496130)
two smaller loops   0.830000   0.000000   0.830000 (  0.831476)
  N.times do
    foo = 1+1
    bar = 2+2
  end
这是2000万次迭代,在每次迭代中都会进行一次计算,总共有4000万次我们称之为操作:

Rehearsal -----------------------------------------------------
one large loop      0.500000   0.010000   0.510000 (  0.508246)
two smaller loops   0.850000   0.000000   0.850000 (  0.852467)
-------------------------------------------- total: 1.360000sec

                        user     system      total        real
one large loop      0.490000   0.000000   0.490000 (  0.496130)
two smaller loops   0.830000   0.000000   0.830000 (  0.831476)
  N.times do
    foo = 1+1
    bar = 2+2
  end
30<40,因此第一个示例更快

较大的循环执行2*10000000个操作,而2个较小的循环中的每个循环执行1*10000000个操作

如果不定义机器模型和成本模型来对这些“操作”进行建模,那么谈论“操作”是没有意义的。或者,简单地说:在你弄清楚自己在数什么之前,数东西是没有意义的

在本例中,您正在计算加法。你是对的:在只计算添加量的模型中,两个版本的添加量相同

但是,它们没有相同数量的块激活

请记住,大致如下所示:

类整数
def时间
返回(uuu被调用方uuu)的枚举u,除非给出块u?
返回自我,除非是积极的?
i=-1
当(i+=1)
因此,对于循环的每次迭代,都会激活传递到
整数倍的块(即
产量

如果我们将其添加为新的“操作”类别,则我们有以下内容:

  • one_循环
    :2000万次添加和10000000次块激活
  • 两个_循环
    :2000万次添加和2000万次块激活
因此,两种方法的加法次数相同,但
two_循环
的块激活次数是后者的两倍

这意味着,我们还必须考虑添加与块激活的相对成本。现在,在语义上,加法只是一个普通的方法调用。激活块有点类似于方法调用

因此,我们预计添加和块激活的成本大致相似,这意味着我们的成本为:

  • one_循环
    :30000000“类似方法调用的操作”
  • two_循环
    :40000000“类似方法调用的操作”
换句话说,我们预计
two_loop
会慢33%,或者
one_loop
会快25%,这取决于您如何看待它

然而,我们实际上发现差异要大得多,所以很明显我们在模型中遗漏了一些东西

我们缺少的是优化。整数上的算术运算非常常见,并且对性能非常关键,因此所有Ruby实现都会竭尽全力使其快速。事实上,在所有Ruby实现中,简单的添加(例如您正在使用的添加)将直接映射到单个CPU
ADD
指令,并且根本不会产生方法调用的开销

块激活在Ruby中也非常重要,因此它们也经过了大量优化,但基本上比添加两个机器字整数要复杂几个数量级

事实上,块激活到机器字整数加法的相对复杂性非常大,以至于我们实际上可以在我们的模型中完全忽略加法:

  • one_循环
    :10000000块激活
  • 两个_循环
    :20000000块激活
这为我们提供了一个2:1的系数,因此我们预计
two_loop
速度将降低100%,或者
one_loop
速度将提高50%

顺便说一下,我忽略了另一个正在发生的操作:局部变量的定义和初始化。参数类似:这是一个速度非常快的操作,与块激活相比可以忽略不计

实际上,到目前为止,我们只讨论了这些操作的相对成本,以及它们如何意味着我们可以忽略添加和局部变量的成本。然而,有一个更强烈的理由可以忽略这些:优化

即使是最简单的Ruby实现也能够完全优化掉局部变量:它们只在一个地方定义和初始化,并且永远不会被访问。它们仅存在于区块f的范围内