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实现中,简单的添加(例如您正在使用的添加)将直接映射到单个CPUADD
指令,并且根本不会产生方法调用的开销
块激活在Ruby中也非常重要,因此它们也经过了大量优化,但基本上比添加两个机器字整数要复杂几个数量级
事实上,块激活到机器字整数加法的相对复杂性非常大,以至于我们实际上可以在我们的模型中完全忽略加法:
one_循环
:10000000块激活
两个_循环
:20000000块激活
这为我们提供了一个2:1的系数,因此我们预计two_loop
速度将降低100%,或者one_loop
速度将提高50%
顺便说一下,我忽略了另一个正在发生的操作:局部变量的定义和初始化。参数类似:这是一个速度非常快的操作,与块激活相比可以忽略不计
实际上,到目前为止,我们只讨论了这些操作的相对成本,以及它们如何意味着我们可以忽略添加和局部变量的成本。然而,有一个更强烈的理由可以忽略这些:优化
即使是最简单的Ruby实现也能够完全优化掉局部变量:它们只在一个地方定义和初始化,并且永远不会被访问。它们仅存在于区块f的范围内