PHP,为人们带来了多少抽象?


乔伊:发生了什么事?
悲伤:我们正在抽象! 有四个阶段。 这是第一。 非客观的碎片!
兵兵:好的,不要惊慌。 重要的是我们大家在一起。 [突然他的抽象手臂掉下来了]
喜悦:哦! [悲伤和喜悦也开始崩溃]
悲伤:我们处于第二阶段。 我们正在解构! [当兵兵摔成碎片时]
兵峰:我感觉不到我的腿! [抬起一只脚]哦,在那里。
©卡通里里外外

每个人都喜欢编写漂亮的代码。 到抽象,lambda,SOLID,DRY,DI等 等 在本文中,我想探讨性能方面的成本以及原因。

为此,执行一项脱离现实的简单任务,逐步将美感带入其中,评估性能并在幕后看待。

免责声明:绝对不应将本文理解为编写不良代码的呼吁。 最好在看完“酷! 现在我知道它的内部了。 但是,当然,我不会使用它。” :)

挑战:

  1. Dan文本文件。
  2. 我们将其分成几行。
  3. 左右修剪空间
  4. 丢弃所有空白行。
  5. 我们将所有非单个空格替换为单个空格(“ ABC”->“ ABC”)。
  6. 根据这些单词,包含10个以上单词的行将向后翻转(“ An Bn Cn”->“ Cn Bn An”)。
  7. 我们计算每行发生多少次。
  8. 打印所有出现超过N次的行。

作为输入文件,按照传统,我们将php-src / Zend / zend_vm_execute.h占用约7万行。

作为运行时,请使用PHP 7.3.6。
让我们在https://3v4l.org上查看编译后的操作码。

将进行以下测量:

//     $start = microtime(true); ob_start(); for ($i = 0; $i < 10; $i++) { //    } ob_clean(); echo "Time: " . (microtime(true) - $start) / 10; 

天真的第一方法


让我们编写一个简单的命令性代码:

 $array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')); $cache = []; foreach ($array as $row) { if (empty($row)) continue; $words = preg_split("/\s+/", trim($row)); if (count($words) > 10) { $words = array_reverse($words); } $row = implode(" ", $words); if (isset($cache[$row])) { $cache[$row]++; } else { $cache[$row] = 1; } } foreach ($cache as $key => $value) { if ($value > 1000) { echo "$key : $value" . PHP_EOL; } } 

运行时间〜0.148 s

一切都很简单,没有什么可谈的。

第二种方法,程序


我们重构代码并在函数中执行基本操作。
我们将努力坚持唯一责任的原则。

扰流板下方的鞋布。
 function getContentFromFile(string $fileName): array { return explode("\n", file_get_contents($fileName)); } function reverseWordsIfNeeded(array &$input) { if (count($input) > 10) { $input = array_reverse($input); } } function prepareString(string $input): string { $words = preg_split("/\s+/", trim($input)); reverseWordsIfNeeded($words); return implode(" ", $words); } function printIfSuitable(array $input, int $threshold) { foreach ($input as $key => $value) { if ($value > $threshold) { echo "$key : $value" . PHP_EOL; } } } function addToCache(array &$cache, string $line) { if (isset($cache[$line])) { $cache[$line]++; } else { $cache[$line] = 1; } } function processContent(array $input): array { $cache = []; foreach ($input as $row) { if (empty($row)) continue; addToCache($cache, prepareString($row)); } return $cache; } printIfSuitable( processContent( getContentFromFile('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h') ), 1000 ); 


运行时〜0.275s ... WTF!? 差异几乎是2倍!

让我们从虚拟机的角度看一下PHP函数是什么。

代码:

 $a = 1; $b = 2; $c = $a + $b; 

编译为:

 line #* EIO op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > ASSIGN !0, 1 3 1 ASSIGN !1, 2 4 2 ADD ~5 !0, !1 3 ASSIGN !2, ~5 

让我们将加法添加到函数中:

 function sum($a, $b){ return $a + $b; } $a = 1; $b = 1; $c = sum($a, $b); 

此类代码被编译为两组操作码:一组用于根名称空间,另一组用于函数。

根目录:

 line #* EIO op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > ASSIGN !0, 1 3 1 ASSIGN !1, 1 5 2 NOP 9 3 INIT_FCALL 'sum' 4 SEND_VAR !0 5 SEND_VAR !1 6 DO_FCALL 0 $5 7 ASSIGN !2, $5 

功能:

 line #* EIO op fetch ext return operands ------------------------------------------------------------------------------------- 5 0 E > RECV !0 1 RECV !1 6 2 ADD ~2 !0, !1 3 > RETURN ~2 

即 即使仅按操作码计数,每个函数调用也会添加3 + 2N个操作码,其中N是传递的参数数。

而且,如果您进行更深入的研究,那么在这里我们还将切换执行上下文。

对重构代码的粗略估算可以得出这样的数字(记住大约有70,000次迭代)。
“附加”执行操作码的数量:〜17,000,000。
上下文切换数:〜280,000。

第三种方法,经典


特别是在没有哲学的情况下,我们将所有这些函数包装在一个类中。

扰流板下方的床单
 class ProcessFile { private $content; private $cache = []; function __construct(string $fileName) { $this->content = explode("\n", file_get_contents($fileName)); } private function reverseWordsIfNeeded(array &$input) { if (count($input) > 10) { $input = array_reverse($input); } } private function prepareString(string $input): string { $words = preg_split("/\s+/", trim($input)); $this->reverseWordsIfNeeded($words); return implode(" ", $words); } function printIfSuitable(int $threshold) { foreach ($this->cache as $key => $value) { if ($value > $threshold) { echo "$key : $value" . PHP_EOL; } } } private function addToCache(string $line) { if (isset($this->cache[$line])) { $this->cache[$line]++; } else { $this->cache[$line] = 1; } } function processContent() { foreach ($this->content as $row) { if (empty($row)) continue; $this->addToCache( $this->prepareString($row)); } } } $processFile = new ProcessFile('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h'); $processFile->processContent(); $processFile->printIfSuitable(1000); 


交货时间:0.297 情况变得更糟。 不多,但值得注意。 创建一个对象(在我们的例子中是10次)是否如此昂贵? Nuuu ...不仅如此。

让我们看看虚拟机如何与类一起工作。

 class Adder{ private $a; private $b; function __construct($a, $b) { $this->a = $a; $this->b = $b; } function sum(){ return $this->a + $this->b; } } $a = 1; $b = 1; $adder = new Adder($a, $b); $c = $adder->sum(); 

将有三组操作码,这是合乎逻辑的:root和两种方法。

根目录:

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > NOP 16 1 ASSIGN !0, 1 17 2 ASSIGN !1, 1 18 3 NEW $7 :15 4 SEND_VAR_EX !0 5 SEND_VAR_EX !1 6 DO_FCALL 0 7 ASSIGN !2, $7 19 8 INIT_METHOD_CALL !2, 'sum' 9 DO_FCALL 0 $10 10 ASSIGN !3, $10 

构造函数:

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 6 0 E > RECV !0 1 RECV !1 7 2 ASSIGN_OBJ 'a' 3 OP_DATA !0 8 4 ASSIGN_OBJ 'b' 5 OP_DATA !1 9 6 > RETURN null 

求和方法:

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 11 0 E > FETCH_OBJ_R ~0 'a' 1 FETCH_OBJ_R ~1 'b' 2 ADD ~2 ~0, ~1 3 > RETURN ~2 

new关键字实际上转换为函数调用(第3-6行)。
它创建该类的实例,并使用传递的参数调用该构造函数。

在方法代码中,我们将对使用类字段感兴趣。 请注意,如果您为一个简单的ASSIGN操作码分配了普通变量进行分配,那么对于类字段,一切都会有所不同。

分配-2个操作码

  7 2 ASSIGN_OBJ 'a' 3 OP_DATA !0 

读取-1个操作码

  1 FETCH_OBJ_R ~1 'b' 

在这里,您应该知道ASSIGN_OBJFETCH_OBJ_R比简单的ASSIGN复杂得多,因此占用更多资源,而简单的ASSIGN只是将zval从一个内存复制到另一个内存。

操作码处理程序的行数(C代码)
ASSIGN_OBJ149
OP_DATA30
FETCH_OBJ_R112
分配26

显然,这样的比较远非正确之举,但仍给出了一些想法。 再进一步,我将进行测量。

现在,让我们看一下实例化一个对象的代价。 让我们测量一百万次迭代:

 class ValueObject{ private $a; function __construct($a) { $this->a = $a; } } $start = microtime(true); for($i = 0; $i < 1000000; $i++){ // $a = $i; // $a = new ValueObject($i); } echo "Time: " . (microtime(true) - $start); 

变量分配:0.092。
实例对象:0.889。

这样的事情。 并非完全免费,尤其是很多次。

好吧,为了避免起床两次,让我们测量使用属性和局部变量之间的差异。 为此,请按以下方式更改我们的代码:

 class ValueObject{ private $b; function try($a) { //    // $this->b = $a; // $c = $this->b; //    // $b = $a; // $c = $b; return $c; } } $a = new ValueObject(); $start = microtime(true); for($i = 0; $i < 1000000; $i++){ $b = $a->try($i); } echo "Simple. Time: " . (microtime(true) - $start); 


通过分配交换:0.830。
通过财产交换:0.862。

只是一点,但是更长。 在将函数包装到类中之后,得到的差异只是相同的顺序。

平凡的结论


  1. 下次要实例化一百万个对象时,请考虑是否真正需要它。 也许只是一个数组,对吧?
  2. 为了节省一毫秒而编写意大利面条代码-嗯,那。 废气很便宜,同事以后可以击败他们。
  3. 但是为了节省500毫秒,也许有时是有道理的。 最主要的是不要走得太远,并记住这500毫秒很可能仅由一小段非常热门的代码节省下来​​,而不是使整个项目陷入一片空白。

PS关于下一次lambdas。 那里很有趣。 :)

Source: https://habr.com/ru/post/zh-CN468021/


All Articles