乔伊:发生了什么事?
悲伤:我们正在抽象! 有四个阶段。 这是第一。 非客观的碎片!
兵兵:好的,不要惊慌。 重要的是我们大家在一起。 [突然他的抽象手臂掉下来了]
喜悦:哦! [悲伤和喜悦也开始崩溃]
悲伤:我们处于第二阶段。 我们正在解构! [当兵兵摔成碎片时]
兵峰:我感觉不到我的腿! [抬起一只脚]哦,在那里。
©卡通里里外外每个人都喜欢编写漂亮的代码。 到抽象,lambda,SOLID,DRY,DI等 等 在本文中,我想探讨性能方面的成本以及原因。
为此,执行一项脱离现实的简单任务,逐步将美感带入其中,评估性能并在幕后看待。
免责声明:绝对不应将本文理解为编写不良代码的呼吁。 最好在看完“酷! 现在我知道它的内部了。 但是,当然,我不会使用它。” :)
挑战:
- Dan文本文件。
- 我们将其分成几行。
- 左右修剪空间
- 丢弃所有空白行。
- 我们将所有非单个空格替换为单个空格(“ ABC”->“ ABC”)。
- 根据这些单词,包含10个以上单词的行将向后翻转(“ An Bn Cn”->“ Cn Bn An”)。
- 我们计算每行发生多少次。
- 打印所有出现超过N次的行。
作为输入文件,按照传统,我们将php-src / Zend / zend_vm_execute.h占用约7万行。
作为运行时,请使用PHP 7.3.6。
让我们在
https://3v4l.org上查看编译后的操作码。
将进行以下测量:
天真的第一方法
让我们编写一个简单的命令性代码:
$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
让我们将加法添加到函数中:
function sum($a, $b){ return $a + $b; } $a = 1; $b = 1; $c = sum($a, $b);
此类代码被编译为两组操作码:一组用于根名称空间,另一组用于函数。
根目录:
line
功能:
line
即 即使仅按操作码计数,每个函数调用也会添加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
构造函数:
line
求和方法:
line
new关键字实际上转换为函数调用(第3-6行)。
它创建该类的实例,并使用传递的参数调用该构造函数。
在方法代码中,我们将对使用类字段感兴趣。 请注意,如果您为一个简单的
ASSIGN操作码分配了普通变量进行分配,那么对于类字段,一切都会有所不同。
分配-2个操作码
7 2 ASSIGN_OBJ 'a' 3 OP_DATA !0
读取-1个操作码
1 FETCH_OBJ_R ~1 'b'
在这里,您应该知道
ASSIGN_OBJ和
FETCH_OBJ_R比简单的
ASSIGN复杂得多,因此
占用更多资源,而简单的
ASSIGN只是将
zval从一个内存复制到另一个内存。
显然,这样的比较远非正确之举,但仍给出了一些想法。 再进一步,我将进行测量。
现在,让我们看一下实例化一个对象的代价。 让我们测量一百万次迭代:
class ValueObject{ private $a; function __construct($a) { $this->a = $a; } } $start = microtime(true); for($i = 0; $i < 1000000; $i++){
变量分配:0.092。
实例对象:0.889。
这样的事情。 并非完全免费,尤其是很多次。
好吧,为了避免起床两次,让我们测量使用属性和局部变量之间的差异。 为此,请按以下方式更改我们的代码:
class ValueObject{ private $b; function try($a) {
通过分配交换:0.830。
通过财产交换:0.862。
只是一点,但是更长。 在将函数包装到类中之后,得到的差异只是相同的顺序。
平凡的结论
- 下次要实例化一百万个对象时,请考虑是否真正需要它。 也许只是一个数组,对吧?
- 为了节省一毫秒而编写意大利面条代码-嗯,那。 废气很便宜,同事以后可以击败他们。
- 但是为了节省500毫秒,也许有时是有道理的。 最主要的是不要走得太远,并记住这500毫秒很可能仅由一小段非常热门的代码节省下来,而不是使整个项目陷入一片空白。
PS关于下一次lambdas。 那里很有趣。 :)