PHP解释器代码中的微优化

时间:2018-11-30 22:28:02

标签: php bytecode micro-optimization

我在so thread上遇到了麻烦,因此决定用PHP编写类似的测试。 我的测试代码是这样的:

// Slow version
$t1 = microtime(true);
for ($n = 0, $i = 0; $i < 20000000; $i++) {
    $n += 2 * ($i * $i);
}
$t2 = microtime(true);
echo "n={$n}\n";

// Optimized version
$t3 = microtime(true);
for ($n = 0, $i = 0; $i < 20000000; $i++) {
    $n += $i * $i;
}
$n *= 2;
$t4 = microtime(true);
echo "n={$n}\n";

$speedup = round(100 * (($t2 - $t1) - ($t4 - $t3)) / ($t2 - $t1), 0);
echo "speedup: {$speedup}%\n";

结果

  1. 在PHP 2 * ($i * $i)版本中,其运行方式与2 * $i * $i类似,
    因此,PHP解释器无法将字节码作为Java中的JVM进行优化
  2. 即使我手动优化了代码-在达到8%速度的情况下, Java版本加快了16%的速度。因此,PHP版本在Java代码中获得了大约1/2的加速因子。

优化理由

我不会详细介绍,但是优化和未优化代码中的乘法比例为->

1个总和:3/4
2个总和:4/6
3个总和:5/8
4个总和:6/10
...

一般来说:

enter image description here

其中,n是循环中的求和数。为了对我们有用的公式-我们需要计算N接近无穷大时的极限(以复制我们在一个循环中进行大量求和的情况)。因此:

enter image description here

因此我们得出结论,在优化的代码中,乘法必须减少50%

问题

  1. 为什么PHP解释器未应用代码优化?
  2. 为什么PHP的加速因子仅为Java的一半?

1 个答案:

答案 0 :(得分:0)

是时候分析由PHP解释器生成的PHP操作码了。为此,您需要安装VLD extension并从命令行使用它来生成php脚本的操作码。

操作码分析

  1. 在操作码和内存使用方面,看来$i++++$i是不同的。语句$ i ++;生成操作码:
 POST_INC ~4 !1
 FREE     ~4

将计数器增加1,并将先前的值保存到内存插槽4中。然后,因为从不使用该值-将其从内存中释放出来。问题-如果从未使用过价值,为什么我们需要存储价值?

  1. 似乎确实存在循环损失,因此我们可以通过执行循环展开来获得额外的性能。

优化的测试代码

将POST_INC更改为ASSIGN_ADD(不会在内存中保存其他信息)并执行循环展开,可以使用以下测试代码:

while (true) {

// Slow version
$t1 = microtime(true);
for ($n = 0, $i = 0; $i < 2000; $i+=10) {
    // loop unrolling
    $n += 2 * (($i+0) * ($i+0));
    $n += 2 * (($i+1) * ($i+1));
    $n += 2 * (($i+2) * ($i+2));
    $n += 2 * (($i+3) * ($i+3));
    $n += 2 * (($i+4) * ($i+4));
    $n += 2 * (($i+5) * ($i+5));
    $n += 2 * (($i+6) * ($i+6));
    $n += 2 * (($i+7) * ($i+7));
    $n += 2 * (($i+8) * ($i+8));
    $n += 2 * (($i+9) * ($i+9));
}
$t2 = microtime(true);
echo "{$n}\n";

// Optimized version
$t3 = microtime(true);
for ($n = 0, $i = 0; $i < 2000; $i+=10) {
    // loop unrolling
    $n += ($i+0) * ($i+0);
    $n += ($i+1) * ($i+1);
    $n += ($i+2) * ($i+2);
    $n += ($i+3) * ($i+3);
    $n += ($i+4) * ($i+4);
    $n += ($i+5) * ($i+5);
    $n += ($i+6) * ($i+6);
    $n += ($i+7) * ($i+7);
    $n += ($i+8) * ($i+8);
    $n += ($i+9) * ($i+9);
}
$n *= 2;
$t4 = microtime(true);
echo "{$n}\n";

$speedup = round(100 * (($t2 - $t1) - ($t4 - $t3)) / ($t2 - $t1), 0);
$table[$speedup]++;

echo "****************\n";
foreach ($table as $s => $c) {
  if ($s >= 0 && $s <= 20)
     echo "$s,$c\n";
}

}

结果

脚本将CPU命中一个或另一个加速值的次数汇总。 当CPU命中率与加速比绘制为图形时,我们得到这样的画面:

enter image description here

因此,脚本很可能会获得10%的加速。这意味着我们的优化导致了 +2%的加速(原始脚本为8%)。

期望

我非常确定我所做的所有事情-可以由PHP JIT'er自动完成。我认为在生成二进制可执行文件时,很难将一对POST_INC / FREE操作码自动更改为一个PRE_INC操作码。同样,PHP JIT'er可以应用循环展开并不是奇迹。这仅仅是优化的开始!

希望PHP 8.0会有一个准时人员