認(rèn)識PHP8
認(rèn)識PHP8
PHP 團(tuán)隊(duì)于2020年11月26日宣布 PHP 8 正式發(fā)布!這意味著將不會有 PHP 7.5 版本。PHP8 目前正處于非常活躍的開發(fā)階段,所以在接下來的幾個月里,情況可能會發(fā)生很大的變化。我也分享一些研究PHP 8 的心得,希望PHPer大家一起共同進(jìn)步。首先說一下最受關(guān)注的JIT。
JIT
由于 PHP 8 是一個新的大版本,因此升級版本,代碼被破壞的可能性更高。如果項(xiàng)目始終保持運(yùn)行 PHP 的最新版本,那么升級相對來說就會輕松很多,因?yàn)樵?7. * 版本中,大多數(shù)重大更改均已棄用。除重大更改外,PHP 8 還帶來了一些不錯的新功能,比如說 JIT 編譯器 , 聯(lián)合類型 , 屬性,以及更多。很多人可能對JIT有很深的誤解,覺得引入JIT之后性能就能提高10倍跟V8平起平坐了,事實(shí)上不是這樣的。JIT技術(shù)的水很深,動態(tài)語言的JIT尤其困難,V8的誕生幾乎可以說是一個技術(shù)奇跡。以PHP社區(qū)的技術(shù)水平,我謹(jǐn)慎地不看好他們解決這個問題的能力,畢竟Facebook的HHVM也沒有完全解決,最后是靠Hacklang補(bǔ)全PHP的語法功能之后才基本圓滿解決的。
動態(tài)語言的JIT本質(zhì)要解決的問題之中,生成匯編只是一小部分,對于弱類型和動態(tài)類型語言來說,優(yōu)化內(nèi)存布局也是重點(diǎn)。例如,對于JavaScript和Python來說,以前對象內(nèi)部是一個HashMap,這種數(shù)據(jù)結(jié)構(gòu)的訪問效率比較低,導(dǎo)致訪問對象的每個屬性都很慢,在JIT之后會將它優(yōu)化成類似C++的平鋪式的布局,將屬性的值按順序放在特定的位置上,這就帶來一些新的要求:
沒有類型標(biāo)注的情況下,JIT只能猜測類型而無法肯定,那么使用優(yōu)化的類型布局之前需要進(jìn)行額外的檢測,判斷是否的確為預(yù)想的類型;
屬性的類型也需要進(jìn)一步推測,使用時也需要檢驗(yàn);
JavaScript、Python乃至PHP都支持在對象創(chuàng)建之后為它添加新的屬性。之前符合推測的類型后來添加或者刪除了屬性,要怎么處理?
除此之外,調(diào)用函數(shù)時候如何優(yōu)化調(diào)用開銷也是一個重點(diǎn),本質(zhì)上跟優(yōu)化對象的內(nèi)存布局是類似的,可以將傳入?yún)?shù)看成是構(gòu)建一個有多個屬性的對象,每個屬性的類型不同。局部變量也需要有選擇性地優(yōu)化到寄存器、棧和堆當(dāng)中。
PHP在這里的優(yōu)勢是支持類型標(biāo)注,缺點(diǎn)是所有Hacklang里面修改掉的部分:
不支持泛型,尤其是array類型不支持泛型。將一個變量類型標(biāo)注為array幾乎沒有任何幫助,PHP中的array可以是順序表也可以是hashmap,還可以混著,value的類型也不確定,這些都對類型優(yōu)化有很高要求。Hacklang就推薦廢掉array改用vector等幾個確定類型且支持泛型的數(shù)據(jù)結(jié)構(gòu)。
reference這個功能,這個功能非常容易成為內(nèi)存布局優(yōu)化的障礙,也會阻礙JIT生成高效代碼,尤其是數(shù)組中可以存儲reference這件事,JIT編譯器完全無法從字面上判斷某條對array元素賦值的語句是否會影響環(huán)境中的其它變量的值。這也是為什么Hacklang直接刪掉了這個功能。
其他參考Hacklang的變更
之前版本(PHP7)摳解釋器實(shí)現(xiàn)帶來的性能優(yōu)化也會是一個阻礙,JIT的時候這些都得放棄掉,因?yàn)閮?nèi)存布局不一樣了,這樣可能導(dǎo)致最初的時候許多應(yīng)用JIT反而變慢。所以,PHP8如果解決不了這些問題,最大的可能是許多microbenchmark速度大幅上升,但整體應(yīng)用性能持平,自娛自樂。
聯(lián)合類型
考慮到 PHP 動態(tài)語言類型的特性,現(xiàn)在很多情況下,聯(lián)合類型都是很有用的。聯(lián)合類型是兩個或者多個類型的集合,表示可以使用其中任何一個類型。
public function foo(Foo|Bar $input): int|float;
聯(lián)合類型中不包含 void,因?yàn)?void 表示的含義是 “根本沒有返回值”。另外,可以使用 |null 或者現(xiàn)有的 ? 表示法來表示包含 nullable 的聯(lián)合體 :
public function foo(Foo|null $foo): void;
public function bar(?Bar $bar): void;
屬性
屬性在其他語言中通常被稱為 注解 ,提供一種在無需解析文檔塊的情況下將元數(shù)據(jù)添加到類中的方法。
use App\Attributes\ExampleAttribute;
<<ExampleAttribute>>
class Foo
{
<<ExampleAttribute>>
public const FOO = 'foo';
<<ExampleAttribute>>
public $x;
<<ExampleAttribute>>
public function foo(<<ExampleAttribute>> $bar) { }
}
新增 mixed 類型
有人將其稱為必要的邪惡產(chǎn)物:因?yàn)閙ixed 類型讓許多人感覺十分混亂。然而,缺少類型在 PHP 中會導(dǎo)致很多情況:
函數(shù)不返回任何內(nèi)容或返回空值
我們需要多種類型的一種類型
我們需要的是 PHP 中不能進(jìn)行類型提示的類型
因?yàn)樯鲜鲈颍砑?mixed 類型是一件很棒的事兒。mixed 本身可以代表下列類型中的任一類型:
array
bool
callable
int
float
null
object
resource
string
請注意,mixed 不僅僅可以用來作為返回類型,還可以用作參數(shù)和屬性類型。
另外,還需要注意,因?yàn)?mixed 類型已經(jīng)包括了 null,因此 mixed 類型不可為空。下面的代碼會觸發(fā)致命錯誤:
// 致命錯誤:混合類型不能為空,null已經(jīng)是混合類型的一部分。
function bar(): ?mixed {}
throw 表達(dá)式
將 throw 從一個語句更改為一個表達(dá)式,這使得可以在很多新地方拋出異常:
$triggerError = fn () => throw new MyError();
$foo = $bar['offset'] ?? throw new OffsetDoesNotExist('offset');
允許對對象使用 ::class
一個很小但是很有用的新特性:現(xiàn)在可以在對象上使用 :: class ,而不必在對象上使用 get_class() ,它的工作方式跟 get_class() 相同。
$foo = new Foo();
var_dump($foo::class);
Non-capturing catches
在 PHP 8 之前,無論何時你想要捕獲一個異常,你都需要先將其存儲到一個變量中,不管這個變量你是否會用到。通過 Non-capturing catches 你可以忽略變量,所以替換下面的代碼:
try {
// 執(zhí)行錯誤代碼段
} catch (MySpecialException $exception) {
Log::error("錯誤");
}
你現(xiàn)在可以這么做:
try {
// 執(zhí)行錯誤代碼段
} catch (MySpecialException) {
Log::error("錯誤");
}
請注意,必須始終指定類型,不允許將 catch 留空,如果你想要捕獲所有類型的異常和錯誤,需要使用 Throwable 作為捕獲類型。
新增 str_contains() 函數(shù)
這是早該出現(xiàn)的函數(shù),我們最終不必再依賴 strpos 來知道一個字符串是否包含另一個字符串。
無需這樣做:
if (strpos('string with lots of words', 'words') !== false) { /* … */ }
現(xiàn)在可以這樣了:
if (str_contains('string with lots of words', 'words')) { /* … */ }
新增 str_starts_with() 和 str_ends_with() 函數(shù)
也是一組早該出現(xiàn)的函數(shù),顧名思義:
str_starts_with('haystack', 'hay'); // true
str_ends_with('haystack', 'stack'); // true
重新分類的錯誤信息
許多以前僅觸發(fā)警告或通知的錯誤已轉(zhuǎn)換為適當(dāng)?shù)腻e誤。以下警告已更改。
變量未定義:Error 異常代替通知
數(shù)組索引未定義:警告代替通知
除以零:DivisionByZeroError 異常代替警告
嘗試添加 / 移除非對象的屬性 '% s' :Error 異常代替警告
嘗試修改非對象的屬性 '% s' :Error 異常代替警告
嘗試分配非對象的屬性 '% s' :Error 異常代替警告
從空值創(chuàng)建默認(rèn)對象:Error 異常代替警告
嘗試獲取非對象的屬性 '% s' :警告代替通知
未定義的屬性:% s::$% s:警告代替通知
無法添加元素到數(shù)組,因?yàn)橄乱粋€元素已被占用:Error 異常代替警告
無法在非數(shù)組變量中銷毀偏移量:Error 異常代替警告
無法將標(biāo)量值用作數(shù)組:Error 異常代替警告
只有數(shù)組和 Traversables 可以被解包:TypeError 異常代替警告
為 foreach () 提供了無效的參數(shù):TypeError 異常代替警告
偏移量類型非法:TypeError 異常代替警告
isset 或 empty 中的偏移量類型非法:TypeError 異常代替警告
unset 中的偏移量類型非法:TypeError 異常代替警告
數(shù)組到字符串的轉(zhuǎn)換:警告代替通知
資源 ID#% d 用作偏移量,轉(zhuǎn)換為整數(shù) (% d):警告代替通知
發(fā)生字符串偏移量轉(zhuǎn)換:警告代替通知
未初始化的字符串偏移量:% d:警告代替通知
無法將空字符串分配給字符串偏移量:Error 異常代替警告
提供的資源不是有效的流資源:TypeError 異常代替警告
@ 運(yùn)算符不再使致命錯誤不提醒
@符是一個偷懶解決問題的辦法,此更改可能會使 PHP 8 之前的版本被 @ 隱藏的錯誤再次顯示出來。請確保在生產(chǎn)服務(wù)器上設(shè)置了 display_errors=Off !
默認(rèn)錯誤報告級別
現(xiàn)在的默認(rèn)錯誤報告級別是 E_ALL 而不是之前的除 E_NOTICE 和 E_DEPRECATED 的所有內(nèi)容。這意味著可能會彈出許多錯誤,這些錯誤以前曾被忽略,盡管在 PHP 8 之前的版本中可能已經(jīng)存在。
默認(rèn) PDO 錯誤模式
這個改動很坑,PDO 的默認(rèn)錯誤模式改為靜默。這意味著當(dāng)出現(xiàn) SQL 錯誤時,除非開發(fā)人員實(shí)現(xiàn)了自己的錯誤處理,否則不會發(fā)出任何錯誤或警告,也不會引發(fā)任何異常。
串聯(lián)優(yōu)先級
在 PHP 7.4 中已廢棄,在8.0開始生效。如果你像這樣子書寫:
echo "sum: " . $a + $b;
PHP 以前會如是理解:
echo ("sum: " . $a) + $b;
PHP 8 :
echo "sum: " . ($a + $b);
暫時就講這些比較有用的新特性吧,一些不常用的就不浪費(fèi)大家時間了。
關(guān)于萬眾期待的JIT,我還想說一些,JIT會讓我的項(xiàng)目更快嗎?
很有可能并不明顯。也許不是我們期望的答案:在一般情況下,用PHP編寫的應(yīng)用程序是I/O綁定的,然而JIT在CPU綁定的代碼上工作得最好。
關(guān)于I/O綁定和CPU綁定最簡單的說法是:
如果我們能夠改進(jìn)(減少、優(yōu)化)它所做的I/O,那么一段I/O綁定的代碼將會運(yùn)行得更快。
如果我們能夠改進(jìn)(減少、優(yōu)化)CPU正在執(zhí)行的指令,或者(神奇地)提高CPU的時鐘速度,那么一段CPU限制的代碼就會運(yùn)行得更快。
一段代碼或一個應(yīng)用程序可以是I/O綁定、CPU綁定,或者與CPU和I/O同等綁定。
一般來說,PHP應(yīng)用程序往往是I/O綁定的——減慢它們速度的是它們正在執(zhí)行的I/O——連接、讀取和寫入數(shù)據(jù)庫、緩存、文件、套接字等等。
PHP實(shí)際上相當(dāng)快,它是世界上解釋速度最快的語言之一。Zend VM調(diào)用與I/O無關(guān)的函數(shù),和在機(jī)器代碼中進(jìn)行相同的調(diào)用之間,沒有顯著的區(qū)別。而PHP的瓶頸也從來不是其他的,正是I/O。
所以JIT好像沒什么用?
其實(shí)不然,引入JIT總體來講是一個積極正面的發(fā)展:
目前已經(jīng)很難通過常規(guī)手段提升 PHP 的性能,JIT 基本上是目前性能提升的唯一手段;
JIT 帶來的性能提升可以讓 PHP 在更多使用場景( CPU 密集)中發(fā)揮作用;
可以使用 PHP 來開發(fā)內(nèi)置函數(shù),而不用擔(dān)心性能方面的問題。這一方面可以加速語言的發(fā)展(更多PHPer可以參與進(jìn)來),同時也可以減少目前使用 C 開發(fā)內(nèi)置函數(shù),容易出現(xiàn)的內(nèi)存管理、溢出等問題。
JIT的引入,對整個語言的使用場景擴(kuò)展,及語言生態(tài)發(fā)展有很深遠(yuǎn)的意義。語言可以有局限,但是人擁有無限可能。許多PHPer把自己局限在web一個角落內(nèi)里。JIT的引入,現(xiàn)在人人都可以去擁抱PHP帶來的轉(zhuǎn)變與生態(tài):Swoole解決了IO密集場景問題,JIT解決了運(yùn)算密集場景問題,未來PHP的發(fā)展更讓人期待。
