PHP 解释器 micro-optimizations 在代码中

PHP interpreter micro-optimizations in code

由于在这个 上绊倒,我决定在 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. in PHP 2 * ($i * $i) 版本运行起来与 2 * $i * $i 非常相似,
    所以 PHP 解释器没有像 Java
  2. 中的 JVM 一样优化字节码
  3. 即使我手动优化代码 - 我也有 ~ 8% 加速,当 Java 的版本得到 ~ 16% 加速。所以 PHP 版本在 Java 的代码中获得了大约 1/2 的加速因子。

优化理由

我不会详细介绍,但是优化和 un-optimized 代码中的乘法比率是 ->

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

一般来说:

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

因此我们得出结论,在优化代码中必须有 50% 更少的乘法运算。

问题

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

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

操作码分析

  1. 似乎 $i++ 在操作码和内存使用方面与 ++$i 不同。声明$i++;生成操作码:
 POST_INC ~4 !1
 FREE     ~4

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

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

优化测试代码

将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 hits vs Speedup 被画成图时,我们得到这样的图:

因此脚本最有可能获得 10% 的加速。这意味着我们的优化导致 +2% 加速(与原始脚本相比 8%)。

期望

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

希望PHP 8.0有一个JIT'er