> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 跨核心記憶體成本

> 跨核心記憶體成本

## 摘要

本文以 [XBOX One 與 XBOX Series X|S](https://learn.microsoft.com/gaming/gdk/docs/gdk-dev/console-dev/overviews/system/XboxOne-vs-ProjectScarlett) 中建立的基礎為本，說明處理器核心之間對共用記憶體進行不同讀取/寫入作業時的相關效能問題與成本。若要複習 XBOX One 系列和 XBOX Series 主機 CPU 的架構、功能與效能，建議您參閱上述文章。

[簡介](#introduction)

[共用資料](#shared-data)

[資料共用的成本](#costs-of-data-sharing)

[建議](#recommendations)

[附錄：程式碼](#appendix-code)

## 簡介

<a id="Introduction" />

***

[XBOX One 與 XBOX Series X|S](https://learn.microsoft.com/gaming/gdk/docs/gdk-dev/console-dev/overviews/system/XboxOne-vs-ProjectScarlett) 說明了與 L1 快取未命中相關的成本。L1 快取未命中最常見的模式之一，是寫入 CPU 核心之間共用的位址，例如在多個執行緒之間共用的資料。本文的目的是提供在核心之間共用可寫入資料所產生之負擔的相對數據。

我們測試的主要結論是，在讀取/寫入作業中，最好將資料限制在每個個別核心，而不是在核心和執行緒之間共用資料。在核心之間共用資料會使程式碼的處理速度變慢，而不是變快。
XBOX One 系列使用 **Jaguar** 處理器，而 XBOX Series 主機使用 **Hercules** 處理器。這兩種處理器都使用 [MOESI protocol - Wikipedia](https://en.wikipedia.org/wiki/MOESI_protocol) 來維持強式記憶體模型和快取一致性。每個快取行可處於下列五種狀態之一。

* Modified (已修改)
  * 此處理器擁有快取行唯一的有效複本，且已進行變更。
  * 快取行與記憶體不相符。
* Owned (已擁有)
  * 此處理器擁有唯一的有效複本，且已進行變更。
  * 其他處理器可能擁有唯讀複本。
* Exclusive (獨佔)
  * 此處理器擁有快取行的唯一複本。
  * 快取行的內容與記憶體相符。
* Shared (共用)
  * 此處理器擁有快取行數個複本中的一個。
  * 如果已進行變更，另一個處理器可能會以 Owned 狀態擁有它
* Invalid (無效)
  * 此快取行無效，必須在存取之前擷取。

如需 AMD 實作的詳細資訊，請參閱 [AMD Developer Central](https://developer.amd.com/resources/developer-guides-manuals/) 上的《AMD64 Architecture Programmer's Manual Volume 2: System Programming》。

## 共用資料

<a id="shared-data" />

***

資料共用可能以多種方式發生。較明顯的方式之一是直接共用變數，例如結構上的參考計數。當每個執行緒透過 shared\_ptr 建立複本時 (例如作為函式呼叫中的參數)，很容易導致資料被修改。光是這項作業就會造成資源爭用。

另一種資料共用稱為偽共用，較難偵測。當兩個記憶體位址解析到相同的快取行時，就會發生偽共用。Jaguar 和 Hercules 處理器的 L1 快取行大小都是 64 位元組，並以 64 位元組對齊，因此同一個 64 位元組區塊中的兩個整數會被視為共用。例如，請考慮下列兩個資料結構：

```cpp theme={null}
class OuterClassSlow
{
    struct InnerStruct
    {
        uint64_t threadOne;
        uint64_t threadTwo;
        uint64_t threadThree;
        uint64_t threadFour;
    };

    InnerStruct dataArray[1024];
};

class OuterClassFast
{
    uint64_t dataArrayThreadOne[1024];
    uint64_t dataArrayThreadTwo[1024];
    uint64_t dataArrayThreadThree[1024];
    uint64_t dataArrayThreadFour[1024];
};
```

threadOne、threadTwo、threadThree 和 threadFour 資料是每個執行緒唯一的。執行緒一只會操作 threadOne 資料，執行緒二只會操作 threadTwo 資料，依此類推。在這兩個資料結構之間，當切換到多執行緒處理時，光是標準寫入作業，OuterClassSlow 的運作速度就可能比 OuterClassFast 慢上二十倍。這是因為偽共用的結果，OuterClassSlow 中的資料全都共用同一個快取行。它們在記憶體中彼此相鄰。這是結構陣列 (AoS) 與陣列結構 (SoA) 之間差異的範例。

<a id="costs-of-data-sharing" />

## 資料共用的成本

***

### 測試設定檔

測試設定檔遵循下列準則：

* 測試的指令包括原始讀取、原始寫入、不可分割載入、不可分割儲存，以及不可分割比較並儲存 (CAS) 作業。每組指令所使用的程式碼提供於[附錄：程式碼](#appendix-code)中。
* 測試以所有執行緒共用的 8 位元組記憶體位置 (共用快取行) 進行，或以每個執行緒擁有唯一的 8 位元組記憶體位置 (唯一快取行) 進行。
* 在緊密迴圈中執行四萬次作業。
* 最佳化工具設定為最小化大小，以停用迴圈展開。
* 總共執行 100 次；下列表格顯示所有執行的中位數。

### 在單一核心上測試

作為基準，我們先在單一核心上測試每項作業。

\| 作業              | XBOX One | XBOX One X | XBOX Series S - 3.4 GHz | XBOX Series X|S - 3.6 GHz | XBOX Series X - 3.8 GHz |
\|--------------|----------:|----------:|----------:|----------:|----------:|
\| **原始讀取**   | 68.78 us  |   52.34 us  |   11.81 us  |   11.16 us  |   10.56 us  |
\| **不可分割載入**  | 68.78 us  | 52.34 us  |   11.81 us  |   11.16 us  |   10.56 us  |
\| **原始寫入**  | 114.61 us  | 104.64 us  |   11.81 us  |   11.16 us  |   10.56 us  |
\| **不可分割儲存**  | 369.55 us  | 278.99 us  |   207.11 us  |   195.61 us  |   185.31 us  |
\| **不可分割 CAS**  | 668.79 us  | 509.02 us  |   203.43 us  |   192.13 us  |   182.03 us  |

首先是 Jaguar 處理器與 Hercules 處理器之間的時間差異。Hercules 的效能最多提升四倍。這是因為更高的時脈速度、更高的頻寬，以及支援更多同時進行中的記憶體作業等綜合因素所致

比較 40,000 次直接讀取與 40,000 次不可分割載入，時間完全沒有差異。這是因為 CPU 的強式記憶體模型和 MOESI 通訊協定。編譯器能夠為這兩項作業產生相同的指令。

此測試中的原始寫入和不可分割儲存作業會嘗試遞增記憶體中的值。在此測試中，原始寫入與不可分割儲存之間的成本增加了兩倍。這是因為不可分割儲存使用 *xchg* 指令，該指令隱含包含 *lock* 前置詞。這可能會使處理器停止，直到結果被排清到快取為止，因為處理器無法跨使用 *lock* 前置詞的指令重新排序作業。原始寫入使用 mov 指令。這不會使處理器停止，處理器可以自由地跨 mov 指令重新排序作業。

接下來是不可分割比較並儲存 (CAS) 作業。此測試的作業與原始寫入和不可分割儲存測試類似，都是嘗試遞增記憶體中的值。此測試的 *cmpxchg* 指令使用 *lock* 前置詞，使其可與不可分割儲存測試相比較。在 Jaguar 處理器上，由於比較的緣故，CAS 作業比先前提到的 *xchg* 成本更高。不過，Hercules 處理器具有改良的實作，使其成本與不可分割儲存相近。

### 在多個核心上測試

多核心測試全都在數種核心設定中使用兩個執行緒。

* 兩個執行緒位於不同的實體核心，但在同一個叢集中。
* 兩個執行緒位於不同的實體核心，且在不同的叢集中。
* 在啟用 SMT 的情況下，兩個執行緒在單一實體處理器的兩個邏輯核心上執行。

我們為每項測試進行了兩次不同的執行。在第一次執行中，兩個執行緒共用相同的 *uint64\_t* 位址。在第二次執行中，每個執行緒都有自己唯一的 *uint64\_t* 位址，不共用快取行。我們這麼做的原因是為了更容易看出處理器之間共用快取行時的效能損失。每項測試的執行緒都同時啟動，且計時僅來自迴圈反覆運算。執行緒也設定為高優先順序，以盡可能降低內容切換的機會。每項測試執行一百次；下列表格顯示中位數結果。

以下提供的數字全都是相對於基準單一核心測試的值，也就是作業在多個核心上執行時成本高出多少。

#### 原始讀取/不可分割載入

這是從記憶體位置直接讀取。執行緒之間沒有爭用，因為沒有任何東西變更資料：每個執行緒在其 L1 快取中都有自己的資料複本。因此，讀取共用資料與讀取唯一資料的時間相同。由於強式記憶體模型，編譯器可以為不可分割載入產生與一般讀取相同的程式碼。這表示每項作業的時間都相同。

\| 測試              | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - 無 SMT |
\|--------------|----------:|----------:|----------:|----------:|----------:|
\| **單一核心**   | 1.00  |    1.00 |    1.00 |    1.00 |
\| **相同實體核心共用**  | 不適用  | 不適用 |    1.97 |    不適用 |
\| **相同叢集共用**  |  1.00 | 1.00 |    1.00 |    1.00 |
\| **跨叢集共用**  | 1.00 |  1.00 |    1.00 |    1.00 |
\| **相同實體核心唯一**  | 不適用 | 不適用 |    1.97 |    不適用 |
\| **相同叢集唯一**  | 1.00 | 1.00 |    1.00 |    1.00 |
\| **跨叢集唯一**  | 1.00 | 1.00 |    1.00 |    1.00 |

從此表格可得出兩個主要結論。

第一，即使多個核心都從相同位置讀取，僅從記憶體讀取對效能沒有影響。每個處理器的快取中都有有效複本，而且由於有多個處理器擁有複本，其快取行處於 Shared 狀態。

第二，在兩個執行緒都在相同實體核心上執行的設定中，時間加倍。啟用 SMT 時，核心的資源由兩個執行緒共用。這可能會帶來效能提升，因為一個執行緒經常無法使用所有可用的資源。不過，此測試是一個非常緊密的迴圈，主導了載入/儲存單元和 L1 快取。單一執行緒就能使用載入/儲存單元中的所有位置，因此兩個執行緒必須以循環配置資源的方式共用這些位置。最終效果是每個執行緒花費兩倍的時間，但核心完成了兩倍的工作。整體而言，核心在相同的時間內完成相同的工作量。

#### 原始寫入

對核心之間共用的記憶體進行簡單寫入，會比單一核心寫入非共用記憶體位置慢得多。寫入作業會使核心更新快取行，並使其他核心上的任何複本失效。

此特定測試是遞增共用記憶體中的值。這表示如果核心的快取中沒有有效複本，就需要從記憶體或該位址的最後寫入者要求資料。這可能會在兩個核心之間產生乒乓效應：一個核心更新資料，第二個核心從第一個核心讀取資料並加以更新，然後第一個核心必須先從該核心讀取資料才能更新。這會在測試迴圈的每次反覆運算中來回持續進行。

乒乓效應的額外負荷可從此表格中看出。

\| 測試              | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - 無 SMT |
\|--------------|----------:|----------:|----------:|----------:|----------:|
\| **單一核心**   | 1.00 |   1.00  |   1.00  |   1.00 |
\| **相同實體核心共用**  | 不適用  | 不適用 |   10.69  |   不適用  |
\| **相同叢集共用**  | 10.54 | 8.73 |   24.36  |    23.59 |
\| **跨叢集共用**  | 13.61 | 12.78 |   18.89  |    23.57 |
\| **相同實體核心唯一**  | 不適用 | 不適用 |   1.98  |   不適用  |
\| **相同叢集唯一**  |  1.05 | 0.93 |   1.03  |    1.03 |
\| **跨叢集唯一**  | 1.01  | 0.84 |   1.04  |    1.03 |

從此表格可得出數個結論。

第一是 Jaguar 與 Hercules 處理器之間對效能的相對影響差異。在 Hercules 上，共用快取行的成本比在 Jaguar 上更高。在此案例中，Jaguar 從另一個核心要求資料的成本為 17 個週期，而相同作業在 Hercules 上的成本為 90 個週期。這是因為 Hercules 的快取較深，具有 Jaguar 所沒有的 L3 快取。

第二是使用共用記憶體位置與唯一位置之間的差異。在此案例中，作業是遞增目前的值，這是讀取/修改/寫入作業。讀取作業必須由最近寫入該記憶體位置的核心來完成。如果不是同一個核心，讀取的成本會大幅提高。如表格所示，成本最多可能高出二十五倍。

第三是在啟用 SMT 的情況下，兩個執行緒共用單一實體核心時，相對差異較小。更新後的快取行不需要從另一個核心擷取，因為它已經在本機。不過，無法套用存放區至載入轉送。當執行緒 A 更新值時，必須先將其排清到快取，執行緒 B 才能使用該值。

第四，在兩個執行緒都在相同實體核心上執行但操作唯一記憶體的設定中，時間加倍。啟用 SMT 時，核心的資源由兩個執行緒共用。這可能會帶來效能提升，因為一個執行緒經常無法使用所有可用的資源。不過，此測試是一個非常緊密的迴圈，主導了載入/儲存單元和 L1 快取。單一執行緒就能使用載入/儲存單元中的所有位置，因此兩個執行緒必須以循環配置資源的方式共用這些位置。最終效果是每個執行緒花費兩倍的時間，但核心完成了兩倍的工作。整體而言，核心在相同的時間內完成相同的工作量。

#### 不可分割儲存

不可分割儲存使用 *xchg* 指令，該指令隱含發出 *lock* 旗標。*xchg* 指令需要先讀取再寫入相關的記憶體位址。在此期間，快取行會被鎖定，防止其他核心存取該快取行。這可能會使擱置中的作業停止，直到 *xchg* 指令將資料寫入快取為止。另一項成本是核心無法跨 *xchg* 指令重新排序作業。最後一項成本發生在另一個核心已修改資料的情況下。在此情況下，需要從該核心擷取資料。在共用快取行上操作時，任何其他核心都需要在整個序列期間等待。

與原始寫入測試一樣，不可分割儲存測試也可能產生類似的乒乓效應，不過在此情況下更為明顯，甚至會影響單一核心測試。
處理器無法推測執行超過迴圈的一次反覆運算。執行緒 A 執行 *xchg* 指令，而執行緒 B 等待存取。執行緒 A 一完成，執行緒 B 就能繼續，並在執行緒 A 為迴圈的下一次反覆運算要求存取之前，立即要求複本。

此表格顯示 *xchg* 指令相較於單一核心執行該作業的相對成本。

\| 測試              | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - 無 SMT |
\|--------------|----------:|----------:|----------:|----------:|----------:|
\| **單一核心**   | 1.00 |   1.00  |   1.00  |   1.00 |
\| **相同實體核心共用**  | 不適用  | 不適用 |    2.38 |   不適用  |
\| **相同叢集共用**  | 6.84 | 6.85 |    4.17 |    4.04 |
\| **跨叢集共用**  | 10.64 | 13.40 |    3.72 |    3.56 |
\| **相同實體核心唯一**  | 不適用 | 不適用 |    0.98 |   不適用  |
\| **相同叢集唯一**  | 1.00 | 1.00 |    1.00 |    1.00 |
\| **跨叢集唯一**  | 1.01 | 1.00 |    1.00 |    1.00  |

從此表格中的資料可得出兩個結論。

相較於原始寫入，使用共用位址時，不可分割儲存的相對時間沒有那麼極端。原因是此測試的大部分成本已在單一核心測試中支付。不過，在 Hercules 上，不可分割儲存的整體成本仍比原始寫入高出二到四倍，在 Jaguar 上則最多高出十三倍。

第二，Hercules 對 *lock* 作業具有改良的實作，在此案例中使相同叢集和跨叢集測試所花費的時間大致相同。當資料在叢集之間共用且使用 *lock* 作業時，Jaguar 處理器的成本最多可能高出兩倍。

#### 不可分割比較並儲存 (CAS)

此測試中的不可分割比較並儲存 (CAS) 作業使用帶有 *lock* 前置詞的 *cmpxchg* 指令，這表示在執行作業時，快取行會被鎖定，其他處理器無法存取。這與不可分割儲存測試中 *xchg* 指令的運作方式相同。

在此測試中，CAS 作業執行與不可分割儲存作業相同的運算。它會嘗試遞增記憶體中的值 (共用或唯一)。唯一的差異是，CAS 作業只會在記憶體位置等於指定的第三個值時才寫入該位置。

\| 測試              | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - 無 SMT |
\|--------------|----------:|----------:|----------:|----------:|----------:|
\| **單一核心**   | 1.00 |   1.00  |   1.00  |   1.00 |
\| **相同實體核心共用**  | 不適用  | 不適用 |    2.91 |   不適用  |
\| **相同叢集共用**  |  4.32 | 4.32 |    4.01 |    4.24 |
\| **跨叢集共用**  |  10.12 | 14.80 |    3.52 |    3.45 |
\| **相同實體核心唯一**  | 不適用 | 不適用 |    0.91 |   不適用  |
\| **相同叢集唯一**  |  1.00 | 1.00 |    1.00 |    1.00 |
\| **跨叢集唯一**  |  1.00 | 1.00 |    1.00 |    1.00 |

由於不可分割儲存和不可分割比較並儲存測試都使用 *lock* 前置詞，因此其相對成本相同，原因也相同。
使用共用位址時，單執行緒與多執行緒作業之間的相對時間沒有那麼極端，因為此測試的大部分成本已在單一核心測試中支付。不過，在 Hercules 上，當資料共用時，CAS 的整體成本仍高出二到四倍，在 Jaguar 上則最多高出十四倍。

第二，Hercules 對 *lock* 作業具有改良的實作，在此案例中使相同叢集和跨叢集測試所花費的時間大致相同。當資料在叢集之間共用且使用 *lock* 作業時，Jaguar 處理器的成本最多可能高出兩倍。

<a id="recommendations" />

## 建議

***

您可以遵循數個關鍵模式，以避免在核心之間共用可寫入資料：

* 資料結構：從使用結構陣列 (AoS) 改為使用陣列結構 (SoA)，讓每個執行緒操作唯一的結構。
* 將唯讀資料與讀取/寫入資料分開。
* 填補：您可以用快取行大小 (64 位元組) 的倍數來填補結構。如果每個執行緒都操作一個結構，這將可消除偽共用。
* 參考計數 (shared\_ptr、weak\_ptr 等)：兩者都有單一的參考計數變數，由物件的所有執行個體共用。
* 清楚界定資料的擁有權。當資料交給另一個執行緒處理時，該執行緒即擁有該資料。在該執行緒處理完成之前，避免對資料進行任何操作。
* 共用工作佇列：盡量避免使用由多個執行緒共用的單一工作佇列，尤其是在工作很小的情況下。請改為考慮為每個執行緒提供唯一的工作佇列。您可以使用工作竊取演算法來協助平衡執行緒之間的負載。

<a id="appendix-code" />

## 附錄：程式碼

***

### 原始讀取

```
for (uint64_t j = 0; j < params.iterations; j++)
{
    fred += *buffer + j;
}

mov         rcx,qword ptr [r8]  
add         rcx,rdx  
inc         rdx  
add         rdi,rcx  
cmp         rdx,r9  
jb          PerfRun::WorkerThreadRead+0D4h (07FF756A2E094h)  
```

### 不可分割載入

```
for (uint64_t j = 0; j < params.iterations; j++)
{
    fred += buffer->load() + j;
}

mov         rax,qword ptr [rsi]  
add         rax,rcx  
inc         rcx  
add         rdi,rax  
cmp         rcx,rdx  
jb          PerfRun::WorkerThreadAtomicRead+0CFh (07FF756A2E4EFh)  
```

### 原始寫入

```
for (uint64_t j = 0; j < params.iterations; j++)
{
    *buffer = *buffer + 1;
}

mov         rcx,qword ptr [r8]  
inc         rcx  
mov         qword ptr [r8],rcx  
sub         rdx,1  
jne         PerfRun::WorkerThreadWrite+0D2h (07FF756A2E212h)  
```

### 不可分割儲存

```
for (uint64_t j = 0; j < params.iterations; j++)
{
    buffer->store(buffer->load() + 1);
}

mov         rcx,qword ptr [rdi]  
inc         rcx  
xchg        rcx,qword ptr [rdi]  
sub         rdx,1  
jne         PerfRun::WorkerThreadAtomicWrite+0CAh (07FF756A2E38Ah)  
```

### 不可分割 CAS

```
for (uint64_t j = 0; j < params.iterations; j++)
{
    uint64_t temp = buffer->load();
    buffer->compare_exchange_strong(temp, temp + 1);
}

mov         rax,qword ptr [rdi]  
lea         rcx,[rax+1]  
lock cmpxchg qword ptr [rdi],rcx  
sub         rdx,1  
jne         PerfRun::WorkerThreadAtomicCAS+0CDh (07FF756A2E68Dh)
```


## Related topics

- [XBOX GDK 遊戲的記憶體系統](/zh-TW/build/console-features/memory/index.md)
- [GDK 中的 Windows Sockets 簡介](/zh-TW/build/console-features/networking/game-mesh/winsock-intro-networking.md)
- [xCurl 概觀](/zh-TW/build/console-features/networking/web-requests/intro-xcurl.md)
- [MsQuic](/zh-TW/build/console-features/networking/game-mesh/msquic-intro-networking.md)
- [WinHTTP 概觀](/zh-TW/build/console-features/networking/web-requests/intro-winhttp.md)
