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";
结果
- in PHP
2 * ($i * $i)
版本运行起来与 2 * $i * $i
非常相似,
所以 PHP 解释器没有像 Java 中的 JVM 一样优化字节码
- 即使我手动优化代码 - 我也有 ~
8%
加速,当
Java 的版本得到 ~ 16%
加速。所以 PHP 版本在 Java 的代码中获得了大约 1/2 的加速因子。
优化理由
我不会详细介绍,但是优化和 un-optimized 代码中的乘法比率是 ->
1 总和:3/4
2 求和:4/6
3 求和:5/8
4 次求和:6/10
...
一般来说:
其中 n 是循环中求和的次数。要成为对我们有用的公式 - 我们需要计算它在 N 接近无穷大时的极限(以复制我们在循环中进行大量求和的情况)。所以:
因此我们得出结论,在优化代码中必须有 50% 更少的乘法运算。
问题
- 为什么 PHP 解释器没有应用代码优化?
- 为什么 PHP 加速因子只有 Java 的一半?
是时候分析 PHP 解释器生成的 PHP 操作码了。为此,您需要安装 VLD extension 并从命令行使用它来生成手头 php 脚本的操作码。
操作码分析
- 似乎
$i++
在操作码和内存使用方面与 ++$i
不同。声明$i++;生成操作码:
POST_INC ~4 !1
FREE ~4
将计数器加 1 并将先前的值保存到内存插槽 #4 中。然后,因为这个值从未被使用过 - 将其从内存中释放出来。问题 - 如果从未使用过,为什么我们需要存储价值?
- 似乎确实存在循环惩罚,因此我们可以通过执行 循环展开.
来获得额外的性能
优化测试代码
将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
由于在这个
// 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";
结果
- in PHP
2 * ($i * $i)
版本运行起来与2 * $i * $i
非常相似,
所以 PHP 解释器没有像 Java 中的 JVM 一样优化字节码
- 即使我手动优化代码 - 我也有 ~
8%
加速,当 Java 的版本得到 ~16%
加速。所以 PHP 版本在 Java 的代码中获得了大约 1/2 的加速因子。
优化理由
我不会详细介绍,但是优化和 un-optimized 代码中的乘法比率是 ->
1 总和:3/4
2 求和:4/6
3 求和:5/8
4 次求和:6/10
...
一般来说:
其中 n 是循环中求和的次数。要成为对我们有用的公式 - 我们需要计算它在 N 接近无穷大时的极限(以复制我们在循环中进行大量求和的情况)。所以:
因此我们得出结论,在优化代码中必须有 50% 更少的乘法运算。
问题
- 为什么 PHP 解释器没有应用代码优化?
- 为什么 PHP 加速因子只有 Java 的一半?
是时候分析 PHP 解释器生成的 PHP 操作码了。为此,您需要安装 VLD extension 并从命令行使用它来生成手头 php 脚本的操作码。
操作码分析
- 似乎
$i++
在操作码和内存使用方面与++$i
不同。声明$i++;生成操作码:
POST_INC ~4 !1 FREE ~4
将计数器加 1 并将先前的值保存到内存插槽 #4 中。然后,因为这个值从未被使用过 - 将其从内存中释放出来。问题 - 如果从未使用过,为什么我们需要存储价值?
- 似乎确实存在循环惩罚,因此我们可以通过执行 循环展开. 来获得额外的性能
优化测试代码
将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