认识PHP8
PHP 团队于2020年11月26日宣布 PHP 8 正式发布!这意味着将不会有 PHP 7.5 版本。PHP8 目前正处于非常活跃的开发阶段,所以在接下来的几个月里,情况可能会发生很大的变化。我也分享一些研究PHP 8 的心得,希望PHPer大家一起共同进步。首先说一下最受关注的JIT。
JIT
由于 PHP 8 是一个新的大版本,因此升级版本,代码被破坏的可能性更高。如果项目始终保持运行 PHP 的最新版本,那么升级相对来说就会轻松很多,因为在 7. * 版本中,大多数重大更改均已弃用。除重大更改外,PHP 8 还带来了一些不错的新功能,比如说 JIT 编译器 , 联合类型 , 属性,以及更多。很多人可能对JIT有很深的误解,觉得引入JIT之后性能就能提高10倍跟V8平起平坐了,事实上不是这样的。JIT技术的水很深,动态语言的JIT尤其困难,V8的诞生几乎可以说是一个技术奇迹。以PHP社区的技术水平,我谨慎地不看好他们解决这个问题的能力,毕竟Facebook的HHVM也没有完全解决,最后是靠Hacklang补全PHP的语法功能之后才基本圆满解决的。
动态语言的JIT本质要解决的问题之中,生成汇编只是一小部分,对于弱类型和动态类型语言来说,优化内存布局也是重点。例如,对于JavaScript和Python来说,以前对象内部是一个HashMap,这种数据结构的访问效率比较低,导致访问对象的每个属性都很慢,在JIT之后会将它优化成类似C++的平铺式的布局,将属性的值按顺序放在特定的位置上,这就带来一些新的要求:
1. 没有类型标注的情况下,JIT只能猜测类型而无法肯定,那么使用优化的类型布局之前需要进行额外的检测,判断是否的确为预想的类型;
2. 属性的类型也需要进一步推测,使用时也需要检验;
3. JavaScript、Python乃至PHP都支持在对象创建之后为它添加新的属性。之前符合推测的类型后来添加或者删除了属性,要怎么处理?
除此之外,调用函数时候如何优化调用开销也是一个重点,本质上跟优化对象的内存布局是类似的,可以将传入参数看成是构建一个有多个属性的对象,每个属性的类型不同。局部变量也需要有选择性地优化到寄存器、栈和堆当中。
PHP在这里的优势是支持类型标注,缺点是所有Hacklang里面修改掉的部分:
1. 不支持泛型,尤其是array类型不支持泛型。将一个变量类型标注为array几乎没有任何帮助,PHP中的array可以是顺序表也可以是hashmap,还可以混着,value的类型也不确定,这些都对类型优化有很高要求。Hacklang就推荐废掉array改用vector等几个确定类型且支持泛型的数据结构。
2. reference这个功能,这个功能非常容易成为内存布局优化的障碍,也会阻碍JIT生成高效代码,尤其是数组中可以存储reference这件事,JIT编译器完全无法从字面上判断某条对array元素赋值的语句是否会影响环境中的其它变量的值。这也是为什么Hacklang直接删掉了这个功能。
3. 其他参考Hacklang的变更
之前版本(PHP7)抠解释器实现带来的性能优化也会是一个阻碍,JIT的时候这些都得放弃掉,因为内存布局不一样了,这样可能导致最初的时候许多应用JIT反而变慢。所以,PHP8如果解决不了这些问题,最大的可能是许多microbenchmark速度大幅上升,但整体应用性能持平,自娱自乐。
联合类型
考虑到 PHP 动态语言类型的特性,现在很多情况下,联合类型都是很有用的。联合类型是两个或者多个类型的集合,表示可以使用其中任何一个类型。
public function foo(Foo|Bar $input): int|float;
联合类型中不包含 void
,因为 void
表示的含义是 “根本没有返回值”。 另外,可以使用 |null
或者现有的 ?
表示法来表示包含 nullable
的联合体 :
public function foo(Foo|null $foo): void;
public function bar(?Bar $bar): void;
属性
属性在其他语言中通常被称为 注解 ,提供一种在无需解析文档块的情况下将元数据添加到类中的方法。
use App\Attributes\ExampleAttribute;
<<ExampleAttribute>>
class Foo
{
<<ExampleAttribute>>
public const FOO = 'foo';
<<ExampleAttribute>>
public $x;
<<ExampleAttribute>>
public function foo(<<ExampleAttribute>> $bar) { }
}
新增 static 返回类型
尽管已经可以返回 self,但是 static 直到 PHP 8 才是有效的返回类型 。考虑到 PHP 具有动态类型的性质,此功能对于许多开发人员将非常有用。
class Foo
{
public function test(): static
{
return new static();
}
}
新增 mixed
类型
有人将其称为必要的邪恶产物:因为mixed
类型让许多人感觉十分混乱。然而,缺少类型在 PHP 中会导致很多情况:
- 函数不返回任何内容或返回空值
- 我们需要多种类型的一种类型
- 我们需要的是 PHP 中不能进行类型提示的类型
因为上述原因,添加 mixed
类型是一件很棒的事儿。mixed
本身可以代表下列类型中的任一类型:
array
bool
callable
int
float
null
object
resource
string
请注意,mixed 不仅仅可以用来作为返回类型,还可以用作参数和属性类型。
另外,还需要注意,因为 mixed 类型已经包括了 null,因此 mixed 类型不可为空。下面的代码会触发致命错误:
// 致命错误:混合类型不能为空,null已经是混合类型的一部分。
function bar(): ?mixed {}
throw
表达式
将 throw
从一个语句更改为一个表达式,这使得可以在很多新地方抛出异常:
$triggerError = fn () => throw new MyError();
$foo = $bar['offset'] ?? throw new OffsetDoesNotExist('offset');
允许对对象使用 ::class
一个很小但是很有用的新特性:现在可以在对象上使用 :: class ,而不必在对象上使用 get_class() ,它的工作方式跟 get_class() 相同。
$foo = new Foo();
var_dump($foo::class);
Non-capturing catches
在 PHP 8 之前,无论何时你想要捕获一个异常,你都需要先将其存储到一个变量中,不管这个变量你是否会用到。通过 Non-capturing catches 你可以忽略变量,所以替换下面的代码:
try {
// 执行错误代码段
} catch (MySpecialException $exception) {
Log::error("错误");
}
你现在可以这么做:
try {
// 执行错误代码段
} catch (MySpecialException) {
Log::error("错误");
}
请注意,必须始终指定类型,不允许将 catch
留空,如果你想要捕获所有类型的异常和错误,需要使用 Throwable
作为捕获类型。
新增 str_contains()
函数
这是早该出现的函数,我们最终不必再依赖 strpos 来知道一个字符串是否包含另一个字符串。
无需这样做:
if (strpos('string with lots of words', 'words') !== false) { /* … */ }
现在可以这样了:
if (str_contains('string with lots of words', 'words')) { /* … */ }
新增 str_starts_with()
和 str_ends_with()
函数
也是一组早该出现的函数,顾名思义:
str_starts_with('haystack', 'hay'); // true
str_ends_with('haystack', 'stack'); // true
重新分类的错误信息
许多以前仅触发警告或通知的错误已转换为适当的错误。以下警告已更改。
- 变量未定义:Error 异常代替通知
- 数组索引未定义:警告代替通知
- 除以零:DivisionByZeroError 异常代替警告
- 尝试添加 / 移除非对象的属性 '% s' :Error 异常代替警告
- 尝试修改非对象的属性 '% s' :Error 异常代替警告
- 尝试分配非对象的属性 '% s' :Error 异常代替警告
- 从空值创建默认对象:Error 异常代替警告
- 尝试获取非对象的属性 '% s' :警告代替通知
- 未定义的属性:% s::$% s:警告代替通知
- 无法添加元素到数组,因为下一个元素已被占用:Error 异常代替警告
- 无法在非数组变量中销毁偏移量:Error 异常代替警告
- 无法将标量值用作数组:Error 异常代替警告
- 只有数组和 Traversables 可以被解包:TypeError 异常代替警告
- 为 foreach () 提供了无效的参数:TypeError 异常代替警告
- 偏移量类型非法:TypeError 异常代替警告
- isset 或 empty 中的偏移量类型非法:TypeError 异常代替警告
- unset 中的偏移量类型非法:TypeError 异常代替警告
- 数组到字符串的转换:警告代替通知
- 资源 ID#% d 用作偏移量,转换为整数 (% d):警告代替通知
- 发生字符串偏移量转换:警告代替通知
- 未初始化的字符串偏移量:% d:警告代替通知
- 无法将空字符串分配给字符串偏移量:Error 异常代替警告
- 提供的资源不是有效的流资源:TypeError 异常代替警告
@ 运算符不再使致命错误不提醒
@符是一个偷懒解决问题的办法,此更改可能会使 PHP 8 之前的版本被 @ 隐藏的错误再次显示出来。请确保在生产服务器上设置了 display_errors=Off !
默认错误报告级别
现在的默认错误报告级别是 E_ALL 而不是之前的除 E_NOTICE 和 E_DEPRECATED 的所有内容。这意味着可能会弹出许多错误,这些错误以前曾被忽略,尽管在 PHP 8 之前的版本中可能已经存在。
默认 PDO 错误模式
这个改动很坑,PDO 的默认错误模式改为静默。这意味着当出现 SQL 错误时,除非开发人员实现了自己的错误处理,否则不会发出任何错误或警告,也不会引发任何异常。
串联优先级
在 PHP 7.4 中已废弃,在8.0开始生效。如果你像这样子书写:
echo "sum: " . $a + $b;
PHP 以前会如是理解:
echo ("sum: " . $a) + $b;
PHP 8 :
echo "sum: " . ($a + $b);
暂时就讲这些比较有用的新特性吧,一些不常用的就不浪费大家时间了。
关于万众期待的JIT,我还想说一些,JIT会让我的项目更快吗?
很有可能并不明显。也许不是我们期望的答案:在一般情况下,用PHP编写的应用程序是I/O绑定的,然而JIT在CPU绑定的代码上工作得最好。
关于I/O绑定和CPU绑定最简单的说法是:
- 如果我们能够改进(减少、优化)它所做的I/O,那么一段I/O绑定的代码将会运行得更快。
- 如果我们能够改进(减少、优化)CPU正在执行的指令,或者(神奇地)提高CPU的时钟速度,那么一段CPU限制的代码就会运行得更快。
- 一段代码或一个应用程序可以是I/O绑定、CPU绑定,或者与CPU和I/O同等绑定。
- 一般来说,PHP应用程序往往是I/O绑定的——减慢它们速度的是它们正在执行的I/O——连接、读取和写入数据库、缓存、文件、套接字等等。
PHP实际上相当快,它是世界上解释速度最快的语言之一。Zend VM调用与I/O无关的函数,和在机器代码中进行相同的调用之间,没有显著的区别。而PHP的瓶颈也从来不是其他的,正是I/O。
所以JIT好像没什么用?
其实不然,引入JIT总体来讲是一个积极正面的发展:
- 目前已经很难通过常规手段提升 PHP 的性能,JIT 基本上是目前性能提升的唯一手段;
- JIT 带来的性能提升可以让 PHP 在更多使用场景( CPU 密集)中发挥作用;
- 可以使用 PHP 来开发内置函数,而不用担心性能方面的问题。这一方面可以加速语言的发展(更多PHPer可以参与进来),同时也可以减少目前使用 C 开发内置函数,容易出现的内存管理、溢出等问题。
JIT的引入,对整个语言的使用场景扩展,及语言生态发展有很深远的意义。语言可以有局限,但是人拥有无限可能。许多PHPer把自己局限在web一个角落内里。JIT的引入,现在人人都可以去拥抱PHP带来的转变与生态:Swoole解决了IO密集场景问题,JIT解决了运算密集场景问题,未来PHP的发展更让人期待。