关于字节码:PHP解释器代码中的微优化

PHP interpreter micro-optimizations in code

通过在这个so线程上绊倒,我决定用PHP编写类似的测试。我的测试代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Slow version
$t1 = microtime(true);
for ($n = 0, $i = 0; $i < 20000000; $i++) {
    $n += 2 * ($i * $i);
}
$t2 = microtime(true);
echo"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}
"
;

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

结果

  • 在php 2 * ($i * $i)版本中,运行方式与2 * $i * $i非常相似,所以PHP解释器没有在Java中优化字节码作为JVM
  • 即使我手动优化代码-我已经加速了~EDOCX1[2],当Java版本获得了EDCOX1×3的加速。所以PHP版本在Java代码中得到大约1/2的加速因子。
  • 优化的基本原理

    我将不再详细讨论,但优化和未优化代码中的乘法比率是->

    1求和:3/42求和:4/63求和:5/84求和:6/10…

    一般情况下:

    enter image description here

    其中n是一个循环中的求和数。为了对我们有用,我们需要计算当n接近无穷大时它的极限(为了复制我们在一个循环中做很多求和的情况)。所以:

    enter image description here

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

    问题

  • 为什么PHP解释器不应用代码优化?
  • 为什么PHP加速因子仅仅是Java中的一半?

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

    Opcode分析

  • 似乎在操作码和内存使用方面,$i++++$i不同。语句$I++;生成操作码:
  • 1
    2
     POST_INC ~4 !1
     FREE     ~4

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

  • 似乎确实存在循环惩罚,所以我们可以通过执行循环展开来获得额外的性能。
  • 优化测试代码

    将post_inc更改为assign_add(它不会在内存中保存附加信息)并执行循环展开,将使用以下测试代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    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}
    "
    ;

    // 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}
    "
    ;

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

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

    }

    结果

    脚本聚合CPU命中到一个或其他加速值的次数。当CPU点击加速比被绘制为图表时,我们得到这样的图片:

    enter image description here

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

    期望

    我非常确定我所做的所有这些事情——都可以通过一个php jit'er自动完成。我认为在生成二进制可执行文件时,很难将一对post_inc/free操作码自动更改为一个pre_inc操作码。此外,php jit'er可以应用循环展开也不是一个奇迹。这只是一个优化的开始!

    希望在php 8.0中有一个jit'er