PHP 內核:foreach 是如何工作的(二)

来源:https://www.cnblogs.com/a609251438/archive/2020/05/20/12926478.html
-Advertisement-
Play Games

PHP 內核:foreach 是如何工作的(一) PHP 5 內部數組指針和散列指針 PHP 5 中的數組有一個專用的 “內部數組指針”(IAP),它適當地支持修改:每當刪除一個元素時,都會檢查 IAP 是否指向該元素。 如果是,則轉發到下一個元素。 雖然 foreach 確實使用了 IAP,但還有 ...


PHP 內核:foreach 是如何工作的(一)

PHP 5

內部數組指針和散列指針

PHP 5 中的數組有一個專用的 “內部數組指針”(IAP),它適當地支持修改:每當刪除一個元素時,都會檢查 IAP 是否指向該元素。 如果是,則轉發到下一個元素。

 

雖然 foreach 確實使用了 IAP,但還有一個複雜因素:只有一個 IAP,但是一個數組可以是多個 foreach 迴圈的一部分:

 

// 在這裡使用by-ref迭代來確保它真的
// 兩個迴圈中的相同數組而不是副本
foreach ($arr as &$v1) {
    foreach ($arr as &$v) {
        // ...
    }
}

為了支持只有一個內部數組指針的兩個同時迴圈,foreach 執行以下 shenanigans:在執行迴圈體之前,foreach 將備份指向當前元素及其散列的指針到每個 foreachHashPointer。迴圈體運行後,如果 IAP 仍然存在,IAP 將被設置回該元素。 但是,如果元素已被刪除,我們將只在 IAP 當前所在的位置使用。這個計劃基本上是可行的,但是你可以從中獲得很多奇怪的情況,其中一些我將在下麵演示。

數組複製

 

IAP 是數組的一個可見特性 (通過 current 系列函數公開),因此 IAP 計數的更改是在寫時複製語義下的修改。不幸的是,這意味著 foreach 在許多情況下被迫複製它正在迭代的數組。 具體條件是:

  1. 數組不是引用(is_ref = 0)。 如果它是一個引用,那麼對它的更改將被傳播,因此不應該複製它。
  2. 數組的 refcount>1。如果 refcount 是 1,那麼此數組是不共用的,我們可以直接修改它。

如果數組沒有被覆制 (is_ref=0, refcount=1),那麼只有它的 refcount 會被增加 (*)。此外,如果使用帶引用的 foreach,那麼 (可能重覆的) 數組將轉換為引用。

如下代碼作為引起複制的示例:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

 

在這裡,$arr 將被覆制以防止 $arr 上的 IAP 更改泄漏到 $outerArr。 就上述條件而言,數組不是引用(is_ref = 0),並且在兩個地方使用(refcount = 2)。 這個要求是不幸的,也是次優實現的工件(這裡不需要修改迭代,因此我們不需要首先使用 IAP)。

 

(*)增加 refcount 聽起來無害,但違反了寫時複製(COW)語義:這意味著我們要修改 refcount = 2 數組的 IAP,而 COW 則要求只能執行修改 on refcount = 1 值。這種違反會導致用戶可見的行為更改 (而 COW 通常是透明的),因為迭代數組上的 IAP 更改將是可見的 -- 但只有在數組上的第一個非 IAP 修改之前。相反,這三個 “有效” 選項是:a) 始終複製,b) 不增加 refcount,從而允許在迴圈中任意修改迭代數組,c) 完全不使用 IAP (PHP 7 解決方案)。

 

位置發展順序

要正確理解下麵的代碼示例,你必須瞭解最後一個實現細節。在偽代碼中,迴圈遍歷某些數據結構的 “正常” 方法是這樣的:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

然而,foreach,作為一個相當特殊的 snowflake,選擇做的事情略有不同:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

也就是說,數組指針 在迴圈體運行之前已經向前移動了。這意味著,當迴圈體處理元素 $i 時,IAP 已經位於元素 $i+1。這就是為什麼在迭代期間顯示修改的代碼示例總是 unset下一個元素,而不是當前元素的原因。

 

例子:你的測試用例

上面描述的三個方面應該可以讓你大致瞭解 foreach 實現的特性,我們可以繼續討論一些例子。

此時,測試用例的行為更容易理解:

  • 在測試用例 1 和 2 中,$array 以 refcount = 1 開始,因此它不會被 foreach 複製:只有 refcount 才會遞增。 當迴圈體隨後修改數組(在該點處具有 refcount = 2)時,將在該點處進行複製。 Foreach 將繼續處理未修改的 $array 副本。
  • 在測試用例 3 中,數組沒有再被覆制,因此 foreach 將修改 $array 變數的 IAP。 在迭代結束時,IAP 為 NULL(意味著迭代已完成),其中 each 返回 false。
  • 在測試用例 4 和 5 中,each 和 reset 都是引用函數。$array 在傳遞給它們時有一個 refcount = 2,所以必須複製它。因此,foreach 將再次處理一個單獨的數組。

例子:current 在 foreach 中的作用

顯示各種複製行為的一個好方法是觀察 foreach 迴圈中 current() 函數的行為。看如下這個例子:

foreach ($array as $val) {
    var_dump(current($array));
}
/* 輸出: 2 2 2 2 2 */

在這裡,你應該知道 current() 是一個 by-ref 函數 (實際上是:preferences-ref),即使它沒有修改數組。它必須很好地處理所有其他函數,如 next,它們都是 by-ref。通過引用傳遞意味著數組必須是分開的,因此 $array 和 foreach-array 將是不同的。你得到是 2 而不是 1 的原因也在上面提到過:foreach在運行用戶代碼之前指向數組指針,而不是之後。因此,即使代碼位於第一個元素,foreach 已經將指針指向第二個元素。

 

現在讓我們嘗試一下小修改:

 

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* 輸出: 2 3 4 5 false */

這裡我們有 is_ref=1 的情況,所以數組沒有被覆制 (就像上面那樣)。但是現在它是一個引用,當傳遞給 by-ref current() 函數時不再需要複製數組。因此,current() 和 foreach 工作在同一個數組上。不過,由於 foreach 指向指針的方式,你仍可以看到 off-by-one 行為。

當執行 by-ref 迭代時,你會得到相同的行為:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* 輸出: 2 3 4 5 false */

 

這裡重要的部分是,當通過引用迭代 $array 時,foreach 會將 $array 設置為 is_ref=1,所以基本上情況與上面相同。

另一個小變化,這次我們將數組分配給另一個變數:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* 輸出: 1 1 1 1 1 */

這裡 $array 的 refcount 在迴圈開始時是 2,所以這一次我們必須在前面進行複製。因此,$array 和 foreach 使用的數組從一開始就完全分離。這就是為什麼 IAP 的位置在迴圈之前的任何位置 (在本例中是在第一個位置)。

例子:迭代期間的修改

 

嘗試理解迭代過程中的修改是我們所有 foreach 問題的起源,因此我們可以拿一些例子來考慮。

 

考慮相同數組上的這些嵌套迴圈 (其中 by-ref 迭代用於確保它確實是相同的):

 

foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// 輸出: (1, 1) (1, 3) (1, 4) (1, 5)

這裡的預期部分是輸出中缺少 (1,2),因為元素 1 被刪除了。可能出乎意料的是,外部迴圈在第一個元素之後停止。這是為什麼呢?

 

這背後的原因是上面描述的嵌套迴圈攻擊:在迴圈體運行之前,當前 IAP 位置和散列被備份到一個 HashPointer 中。在迴圈體之後,它將被恢復,但是只有當元素仍然存在時,否則將使用當前 IAP 位置 (無論它是什麼)。在上面的例子中,情況正是這樣:外部迴圈的當前元素已經被刪除,所以它將使用 IAP,而內部迴圈已經將 IAP 標記為 finished !

 

HashPointer 備份 + 恢復機制的另一個結果是,通過 reset() 等方法更改 IAP。通常不會影響 foreach。例如,下麵的代碼執行起來就像根本不存在 reset() 一樣:

 

$array = [1, 2, 3, 4, 5];

foreach ($array as &$value) {

var_dump($value);

reset($array);

}

// 輸出: 1, 2, 3, 4, 5

 

原因是,當 reset() 暫時修改 IAP 時,它將恢復到迴圈體後面的當前 foreach 元素。要強制 reset() 對迴圈產生影響,你必須刪除當前元素,這樣備份 / 恢復機制就會失敗:

 

$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
    var_dump($value);
    unset($array[1]);
    reset($array);
}
// 輸出: 1, 1, 3, 4, 5

 

但是,這些例子仍是合理的。如果你還記得 HashPointer 還原使用指向元素及其散列的指針來確定它是否仍然存在,那麼真正的樂趣就開始了。但是:散列有衝突,指針可以重用!這意味著,通過仔細選擇數組鍵,我們可以讓 foreach 相信被刪除的元素仍然存在,因此它將直接跳轉到它。一個例子:

 

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    reset($array);
    var_dump($value);
}
// 輸出: 1, 4

 

這裡根據前面的規則,我們通常期望輸出 1,1,3,4。實際情況上'FYFY' 具有與刪除的元素'FYFY' 相同的散列,而分配器恰好重用相同的記憶體位置來存儲元素。因此,foreach 最終直接跳轉到新插入的元素,從而縮短了迴圈。

在迴圈期間替換迭代實體

我想提到的最後一個奇怪的情況是,PHP 允許你在迴圈期間替換迭代實體。所以你可以開始在一個數組上迭代然後在中間用另一個數組替換。或者用一個對象來替換:

$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];

$ref =& $arr;
foreach ($ref as $val) {
    echo "$val\n";
    if ($val == 3) {
        $ref = $obj;
    }
}
/* 輸出: 1 2 3 6 7 8 9 10 */

正如你在本例中所看到的,一旦替換髮生,PHP 將從頭開始迭代另一個實體。

PHP 7

散列表迭代器

 

如果你還記得,數組迭代的主要問題是如何處理迭代過程中元素的刪除。PHP 5 為此使用了一個內部數組指針 (IAP),這有點不太理想,因為一個數組指針必須被拉伸以支持多個同時進行的 foreach 迴圈和與 reset() 等的交互。最重要的是。

 

PHP 7 使用了一種不同的方法,即支持創建任意數量的外部安全散列表迭代器。這些迭代器必須在數組中註冊,從這一點開始,它們具有與 IAP 相同的語義:如果刪除了一個數組元素,那麼指向該元素的所有 hashtable 迭代器都將被提升到下一個元素。

 

這意味著 foreach 將不再使用 IAP。foreach 迴圈絕對不會影響 current() 等的結果。它自己的行為永遠不會受到像 reset() 等函數的影響。

數組複製

 

PHP 5 和 PHP 7 之間的另一個重要更改與數組複製有關。現在 IAP 不再使用了,在所有情況下,按值數組迭代將只執行 refcount 增量 (而不是複製數組)。如果數組在 foreach 迴圈期間被修改,那麼此時將發生複製 (根據寫時複製),而 foreach 將繼續處理舊數組。

 

在大多數情況下,這種更改是透明的,除了更好的性能之外沒有其他效果。但是,有一種情況會導致不同的行為,即數組前是一個引用:

 

$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
    var_dump($val);
    $array[2] = 0;
}
/* 舊輸出: 1, 2, 0, 4, 5 */
/* 新輸出: 1, 2, 3, 4, 5 */

 

以前,引用數組的按值迭代是一種特殊情況。在本例中,沒有發生重覆,因此在迭代期間對數組的所有修改都將由迴圈反映出來。在 PHP 7 中,這種特殊情況消失了:數組的按值迭代將始終繼續處理原始元素,而不考慮迴圈期間的任何修改。

 

當然,這不適用於 by-reference 迭代。如果你通過引用進行迭代,那麼所有的修改都將被迴圈所反映。有趣的是,對於普通對象的按值迭代也是如此:

 

$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
    var_dump($val);
    $obj->bar = 42;
}
/* 新舊輸出: 1, 42 */

 

這反映了對象的按句柄語義 (即,即使在按值上下文中,它們的行為也類似於引用)。

例子

 

讓我們考慮幾個例子,從你的測試用例開始:

 

測試用例 1 和 2 輸出相同:按值數組迭代始終在原始元素上工作。(在本例中,甚至 refcounting 和複製行為在 PHP 5 和 PHP 7 之間也是完全相同的)。

 

測試用例 3 的變化:Foreach 不再使用 IAP,因此 each() 不受迴圈影響。前後輸出一樣。

 

測試用例 4 和 5 保持不變:each() 和 reset() 將在更改 IAP 之前複製數組,而 foreach 仍然使用原始數組。(即使數組是共用的,IAP 的更改也無關緊要。)

 

第二組示例與 current() 在不同 reference/refcounting 配置下的行為有關。這不再有意義,因為 current() 完全不受迴圈影響,所以它的返回值總是保持不變。

 

然而,當考慮迭代過程中的修改時,我們得到了一些有趣的變化。我希望你會發現新的行為更加健全。 第一個例子:

 

$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// 舊輸出: (1, 1) (1, 3) (1, 4) (1, 5)
// 新輸出: (1, 1) (1, 3) (1, 4) (1, 5)
//        (3, 1) (3, 3) (3, 4) (3, 5)
//        (4, 1) (4, 3) (4, 4) (4, 5)
//        (5, 1) (5, 3) (5, 4) (5, 5) 

 

如你所見,外部迴圈在第一次迭代之後不再中止。原因是現在兩個迴圈都有完全獨立的 hashtable 散列表迭代器,並且不再通過共用的 IAP 對兩個迴圈進行交叉污染。

 

現在修複的另外一個奇怪的邊緣現象是,當刪除並且添加恰好具有相同的哈希元素時,會得到奇怪的結果:

 

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    var_dump($value);
}
// 舊輸出: 1, 4
// 新輸出: 1, 3, 4

 

之前的 HashPointer 恢復機制直接跳轉到新元素,因為它 “看起來” 和刪除的元素相同(由於哈希和指針衝突)。由於我們不再依賴於哈希元素,因此不再是一個問題。

 


騰訊T3-T4標準精品PHP架構師教程目錄大全,只要你看完保證薪資上升一個臺階(持續更新)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2.Python數據類型 對於任何一種編程語言的學習,需要打好入門的基礎知識,在Python中常見的基礎知識有 基本數據類型 、 條件語句 、 迴圈語句 和 函數 ,更深層次需要掌握面向對象的知識。今天我們先來瞭解學習一下基礎知識,在Python常見的基本數據類型如下所示: 2.1 基本數據類型 2 ...
  • 從今天開始,我們開始溫習Python吧。 1.變數 1.1 變數定義 變數是編程中最基本的存儲單位,將會暫時存儲你放進去的數據。示意圖如下所示: 在Python中,定義一個變數並完成賦值非常簡單,如下所示: Python中的變數無需事先聲明變數數據類型,在運行時會自動根據變數值進行自行推斷 1.2 ...
  • 1 簡介 單元測試是保證代碼質量的重要一環,而如何衡量單元測試寫得好不好呢? 是一個重要指標。而 則是專門為 提供的用於檢測測試覆蓋率的工具,英文全稱為 。 本文將講解如何在 項目中整合 ,併在 中展示。 的安裝可以參考這篇文章: 《 "Docker搭建代碼檢測平臺SonarQube並檢測maven ...
  • "TCP/IP" "TCP/IP模型" "TCP三次握手與四次握手" "TCP 如何保證可靠傳輸" "HTTP協議" "基本介紹" "工作原理" "HTTP特性" "請求方法" "HTTP 狀態碼" "URL" "HTTP與HTTPS的區別" "RESTful" TCP/IP TCP/IP模型 TC ...
  • 註冊支付寶賬號(個人或企業) 註意:個人只能測試,企業可以正式用於項目中 進入www.alipay.com,選擇:我是支付寶商家 支付寶掃碼登錄(由於我是測試環境,因此直接用的個人賬號) 如果是企業,就選擇:商家中心-產品中心-電腦網站支付-然後開通 如果是個人,就選擇:螞蟻金服開放平臺-網頁移動應 ...
  • 因某項目需要,需要採集微弱的電壓信號,且對電壓精度要求較高,於是選中MCP3421這款18 bit 高精度IIC AD轉換晶元。本文將結合MCP3421的手冊,對該晶元的使用進行詳細解釋,並配合Proteus,完成基於MCP3421的模擬。 關鍵詞:MCP3421, Proteus,MSP430,... ...
  • 引言 c語言編譯運行慢怎麼辦?可能就是這編譯運行的3s鐘就可以把你逼瘋 解決方法 1. 檢查編譯器版本, 優先選擇64位, 即MinGW 64 2. 關閉殺毒軟體, 如Windows Defender, 電腦管家, 或者把文件夾添加進信任區 以下是做的實驗: 開啟電腦管家後一鍵編譯和運行需要3s左右 ...
  • 今天下午做的一個功能,要用到模糊查詢,欄位是description,剛開始我的寫法用的是sql中的模糊查詢語句, 但是這個有問題,只有將欄位的全部值傳入其中,才能查詢,所以不是迷糊查詢。 後來經過搜索,發現要加上一個concat欄位,要先將字元串拼接後,才能實現模糊查詢。 改成這個樣子後,模糊查詢功 ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...