認識PHP8

有痣青年發表於2021-01-05

  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總體來講是一個積極正面的發展:

  1. 目前已經很難通過常規手段提升 PHP 的效能,JIT 基本上是目前效能提升的唯一手段;
  2. JIT 帶來的效能提升可以讓 PHP 在更多使用場景( CPU 密集)中發揮作用;
  3. 可以使用 PHP 來開發內建函式,而不用擔心效能方面的問題。這一方面可以加速語言的發展(更多PHPer可以參與進來),同時也可以減少目前使用 C 開發內建函式,容易出現的記憶體管理、溢位等問題。

  JIT的引入,對整個語言的使用場景擴充套件,及語言生態發展有很深遠的意義。語言可以有侷限,但是人擁有無限可能。許多PHPer把自己侷限在web一個角落內裡。JIT的引入,現在人人都可以去擁抱PHP帶來的轉變與生態:Swoole解決了IO密集場景問題,JIT解決了運算密集場景問題,未來PHP的發展更讓人期待。