【问题标题】:PHP interpreter micro-optimizations in code代码中的 PHP 解释器微优化
【发布时间】:2019-05-03 02:16:49
【问题描述】:

偶然发现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
...

一般来说:

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

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

问题

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

【问题讨论】:

    标签: php bytecode micro-optimization


    【解决方案1】:

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

    操作码分析

    1. 似乎$i++++$i 在操作码和内存使用方面不一样。语句 $i++;生成操作码:
    POST_INC ~4 !1 免费〜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 hits vs Speedup 绘制成图表时,我们得到这样的图:

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

    期望

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

    希望PHP 8.0 会有 JIT'er

    【讨论】:

    • “表现出色”是一种奇怪的表达方式。您只显示相对加速,而不是绝对加速。不因循环中的额外工作而减慢速度通常是的事情(例如,因为智能优化器可以为您将乘法从循环中提取出来,或者因为它创建了免费的高效 asm作为添加的一部分,例如在编译到 x86 asm 时使用 LEA 而不是 ADD)。
    • 不要因为术语而唠叨。我只是想到 16 > 8,就是这样。而且,是的,我同意不因循环中的额外工作而减速是一件好事。但是要发生这种情况,我们需要一个像您所说的体面的优化器,并且在 PHP 中使用 JIT'er 将为优化提供更多选择。顺便说一句,0% 加速也可能意味着 两个循环运行缓慢,因为瓶颈在其他地方(不在额外操作中)- 类型转换等(你自己知道,我相信)
    • 是的,我抱怨 only 显示相对加速的主要原因是 0% 可能意味着两者都运行缓慢。碰巧在 this 一种情况下,编译为机器代码的语言会获得更大的加速,但我认为对于一般情况下得出这样的结论是不合理的。这里没有明显的因果关系或机制。 Java 获得了更大的加速,因为它的 JIT 编译器是平庸的并且会自欺欺人(有太多的展开和缺少优化),而不是因为它。 C 依赖于编译器(因为有多个好的)。
    • 我猜在某些情况下,我们可以构建一个好的优化编译器可以优化掉很多东西(因此编写循环体的更复杂的方法不会导致减速),但是解释器并没有摆脱它并且速度较慢。我只是认为你的结论的要点除了发生在这个测试中的数据之外没有任何理由。
    • 我研究了 PHP 内部结构。似乎您的第二种情况是正确的 - 循环开销和内存管理的其他问题,埋没额外操作的成本
    猜你喜欢
    • 1970-01-01
    • 2017-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-22
    相关资源
    最近更新 更多