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

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

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

 

Tags: