最近我又听说 php 人们仍然在谈论单引号和双引号,并且使用单引号只是一种微观优化,但如果你习惯一直使用单引号,你会节省大量的 cpu 周期!
“一切都已经说过了,但还没有被所有人说”——karl valentin
正是本着这种精神,我正在写一篇关于 nikita popov 12 年前已经做过的同一主题的文章(如果您正在阅读他的文章,您可以在这里停止阅读)。
毛茸茸的到底是什么?
php 执行字符串插值,在字符串中搜索变量的使用情况,并将其替换为所使用变量的值:
$juice = "apple"; echo "they drank some $juice juice."; // will output: they drank some apple juice.
此功能仅限于双引号和定界符中的字符串。使用单引号(或 nowdoc)会产生不同的结果:
$juice = "apple"; echo 'they drank some $juice juice.'; // will output: they drank some $juice juice.
看一下:php 不会搜索该单引号字符串中的变量。所以我们可以开始在任何地方使用单引号。所以人们开始建议这样的改变..
- $juice = "apple"; + $juice = 'apple';
.. 因为它会更快,并且每次执行该文件都会节省大量 cpu 周期,因为 php 不会在单引号字符串中查找变量(无论如何,在示例中不存在)并且每个人都是很高兴,案件已结。
案件结案了吗?
显然,使用单引号和双引号是有区别的,但为了理解发生了什么,我们需要更深入地挖掘。
尽管 php 是一种解释性语言,但它使用编译步骤,其中某些部分一起运行以获得虚拟机实际可以执行的东西,即操作码。那么我们如何从 php 源代码获取操作码呢?
词法分析器
词法分析器扫描源代码文件并将其分解为标记。可以在 token_get_all() 函数文档中找到该含义的简单示例。 <?php echo ""; 的 php 源代码变成这些代币:
t_open_tag (<?php ) t_echo (echo) t_whitespace ( ) t_constant_encapsed_string ("")
我们可以在这个 3v4l.org 代码片段中看到它的实际效果并使用它。
解析器
解析器获取这些标记并从中生成抽象语法树。当表示为 json 时,上述示例的 ast 表示如下所示:
{ "data": [ { "nodetype": "stmt_echo", "attributes": { "startline": 1, "starttokenpos": 1, "startfilepos": 6, "endline": 1, "endtokenpos": 4, "endfilepos": 13 }, "exprs": [ { "nodetype": "scalar_string", "attributes": { "startline": 1, "starttokenpos": 3, "startfilepos": 11, "endline": 1, "endtokenpos": 3, "endfilepos": 12, "kind": 2, "rawvalue": """" }, "value": "" } ] } ] }
如果你也想玩这个,看看其他代码的 ast 是什么样子,我找到了 ryan chandler 的 https://phpast.com/ 和 https://php-ast-viewer.com/ ,它们都显示了你是一段给定的 php 代码的 ast。
编译器
编译器获取 ast 并创建操作码。操作码是虚拟机执行的内容,如果您进行了设置并启用了它,它也会存储在 opcache 中(我强烈推荐)。
要查看操作码,我们有多个选项(也许更多,但我确实知道这三个):
- 使用 vulcan 逻辑转储器扩展。它也被纳入 3v4l.org
- 使用 phpdbg -p script.php 转储操作码
- 或者使用 opcache 的 opcache.opt_debug_level ini 设置使其打印出操作码
- 0x10000 的值输出优化前的操作码
- 0x20000 的值输出优化后的操作码
$ echo '<?php echo "";' > foo.php $ php -dopcache.opt_debug_level=0x10000 foo.php $_main: ... 0000 echo string("") 0001 return int(1)
假设
回到使用单引号与双引号时节省 cpu 周期的最初想法,我想我们都同意,只有当 php 在运行时为每个请求评估这些字符串时,这才是正确的。
运行时会发生什么?
所以让我们看看 php 为两个不同版本创建了哪些操作码。
双引号:
<?php echo "apple";
0000 echo string("apple") 0001 return int(1)
对比单引号:
<?php echo 'apple';
0000 echo string("apple") 0001 return int(1)
嘿等等,奇怪的事情发生了。这看起来一模一样!我的微优化去哪儿了?
好吧,也许 echo 操作码处理程序的实现会解析给定的字符串,尽管没有标记或其他东西告诉它这样做......嗯?
让我们尝试不同的方法,看看词法分析器对这两种情况做了什么:
双引号:
t_open_tag (<?php ) t_echo (echo) t_whitespace ( ) t_constant_encapsed_string ("")
对比单引号:
line 1: t_open_tag (<?php ) line 1: t_echo (echo) line 1: t_whitespace ( ) line 1: t_constant_encapsed_string ('')
标记仍然区分双引号和单引号,但检查 ast 将为我们提供两种情况相同的结果 - 唯一的区别是 scalar_string 节点属性中的 rawvalue,它仍然具有单/双引号,但值在这两种情况下都使用双引号。
新假设
难道字符串插值实际上是在编译时完成的吗?
让我们看一个稍微“复杂”的例子:
<?php $juice="apple"; echo "juice: $juice";
此文件的令牌是:
t_open_tag (<?php ) t_variable ($juice) t_constant_encapsed_string ("apple") t_whitespace () t_echo (echo) t_whitespace ( ) t_encapsed_and_whitespace (juice: ) t_variable ($juice)
看看最后两个标记!字符串插值是在词法分析器中处理的,因此是编译时的事情,与运行时无关。
为了完整起见,让我们看一下由此生成的操作码(优化后,使用 0x20000):
0000 assign cv0($juice) string("apple") 0001 t2 = fast_concat string("juice: ") cv0($juice) 0002 echo t2 0003 return int(1)
这与我们简单的 <?php echo ""; 中的操作码不同。例如,但这没关系,因为我们在这里做一些不同的事情。
进入正题:我应该连接还是插值?
让我们看看这三个不同的版本:
<?php $juice = "apple"; echo "juice: $juice $juice"; echo "juice: ", $juice, " ", $juice; echo "juice: ".$juice." ".$juice;
- 第一个版本使用字符串插值
- 第二个是使用逗号分隔(据我所知,它仅适用于 echo 而不是分配变量或其他任何东西)
- 第三个选项使用字符串连接
第一个操作码将字符串“apple”分配给变量 $juice:
0000 assign cv0($juice) string("apple")
第一个版本(字符串插值)使用绳索作为底层数据结构,经过优化以尽可能少地复制字符串。
0001 t2 = rope_init 4 string("juice: ") 0002 t2 = rope_add 1 t2 cv0($juice) 0003 t2 = rope_add 2 t2 string(" ") 0004 t1 = rope_end 3 t2 cv0($juice) 0005 echo t1
第二个版本是最有效的内存,因为它不创建中间字符串表示。相反,它会对 echo 进行多次调用,从 i/o 角度来看,这是一个阻塞调用,因此根据您的用例,这可能是一个缺点。
0006 echo string("juice: ") 0007 echo cv0($juice) 0008 echo string(" ") 0009 echo cv0($juice)
第三个版本使用 concat/fast_concat 创建中间字符串表示形式,因此可能比绳索版本使用更多的内存。
0010 T1 = CONCAT string("juice: ") CV0($juice) 0011 T2 = FAST_CONCAT T1 string(" ") 0012 T1 = CONCAT T2 CV0($juice) 0013 ECHO T1
那么……这里正确的做法是什么?为什么是字符串插值?
字符串插值在 echo "juice: $juice" 的情况下使用 fast_concat;或在 echo "juice: $juice $juice"; 的情况下高度优化的 rope_* 操作码;但最重要的是它清楚地传达了意图,并且这些都不是我迄今为止使用过的任何 php 应用程序的瓶颈,所以这些实际上都不重要。
总长dr
字符串插值是编译时的事情。诚然,如果没有 opcache,词法分析器将必须在每个请求上检查双引号字符串中使用的变量,即使没有任何变量,也会浪费 cpu 周期,但说实话:问题不是双引号字符串,而是不使用 opcache!
但是,有一个警告:php 高达 4(我相信甚至包括 5.0,甚至可能是 5.1,我不知道)在运行时进行字符串插值,所以使用这些版本......嗯,我想如果有人真的仍然使用 php 5,与上面相同:问题不是双引号字符串,而是使用过时的 php 版本。
最终建议
更新到最新的php版本,启用opcache,从此幸福快乐地生活!
以上就是双引号是否过多,这就是问题所在!的详细内容,更多请关注php中文网其它相关文章!