考慮以下程式:
#include <stdio.h>
#include <stdlib.h>
typedef unsigned long long u64;
int program_1(u64* a, u64* b)
{
const u64 lim = 50l * 1000l * 1000l;
// Reads arrays
u64 sum = 0;
for (u64 i = 0; i < lim * 100; i) {
sum = a[i % lim];
sum = b[i % lim];
}
printf("%llu\n", sum);
return 0;
}
int program_2(u64* a, u64* b)
{
const u64 lim = 50l * 1000l * 1000l;
// Reads arrays
u64 sum = 0;
for (u64 i = 0; i < lim * 100; i) {
sum = a[i % lim];
}
for (u64 i = 0; i < lim * 100; i) {
sum = b[i % lim];
}
printf("%llu\n", sum);
return 0;
}
兩個程式是相同的:它們用 1 填充一個陣列,然后讀取每個元素 100 次,添加到計數器中。唯一的區別是第一個熔斷加法器回圈,而第二個執行兩次單獨的傳遞。鑒于 M1 有 64KB 的 L1 資料快取,我的理解是會發生以下情況:
方案一
sum = a[0] // CACHE MISS. Load a[0..8192] on L1.
sum = b[0] // CACHE MISS. Load b[0..8192] on L1.
sum = a[1] // CACHE MISS. Load a[0..8192] on L1.
sum = b[1] // CACHE MISS. Load b[0..8192] on L1.
sum = a[2] // CACHE MISS. Load a[0..8192] on L1.
sum = b[2] // CACHE MISS. Load b[0..8192] on L1.
(...)
方案二
sum = a[0] // CACHE MISS. Load a[0..8192] on L1.
sum = a[1] // CACHE HIT!
sum = a[2] // CACHE HIT!
sum = a[3] // CACHE HIT!
sum = a[4] // CACHE HIT!
...
sum = a[8192] // CACHE MISS. Load a[8192..16384] on L1.
sum = a[8193] // CACHE HIT!
sum = a[8194] // CACHE HIT!
sum = a[8195] // CACHE HIT!
sum = a[8196] // CACHE HIT!
...
...
sum = b[0] // CACHE MISS. Load b[0..8192] on L1.
sum = b[1] // CACHE HIT!
sum = b[2] // CACHE HIT!
sum = b[3] // CACHE HIT!
sum = b[4] // CACHE HIT!
...
這會讓我相信第一個程式更慢,因為每次讀取都是快取未命中,而第二個主要由快取命中組成。然而,結果不同。在 Macbook Pro M1 上運行,使用clang -O2,第一個程式需要 2.8s 才能完成,而第二個程式大約需要 3.8s。
我關于 L1 快取如何作業的心智模型有什么問題?
uj5u.com熱心網友回復:
我希望:
a) 當 CPU 正在等待資料被提取到 L1 時,sum = a[i % lim];它可以要求將資料提取sum = b[i % lim];到 L1。本質上; 程式 1 并行等待 2 次快取未命中,而程式 2 一次等待 1 次快取未命中,速度可能會慢兩倍。
b) 回圈開銷(在 中的所有作業for (u64 i = 0; i < lim * 100; i) {)和索引(計算i%lim)在程式 2 中重復;導致程式 2 執行幾乎兩倍的額外作業(這與快取未命中無關)。
c) 編譯器不擅長優化。我很驚訝沒有為兩個版本生成相同的代碼。我很震驚 CLANG 和 GCC 都沒有設法自動矢量化(使用 SIMD)。一個非常假設的理想化完美編譯器應該能夠將兩個版本一直優化到write(STDOUT_FILENO, "10000000000\n", 12); return 0.
我關于 L1 快取如何作業的心智模型有什么問題?
看起來您認為快取一次只能快取一件事。對于程式 1,它更像是:
sum = a[0] // CACHE MISS
sum = b[0] // CACHE MISS
sum = a[1] // CACHE HIT (data still in cache)
sum = b[1] // CACHE HIT (data still in cache)
sum = a[2] // CACHE HIT (data still in cache)
sum = b[2] // CACHE HIT (data still in cache)
sum = a[3] // CACHE HIT (data still in cache)
sum = b[3] // CACHE HIT (data still in cache)
sum = a[4] // CACHE HIT (data still in cache)
sum = b[4] // CACHE HIT (data still in cache)
sum = a[5] // CACHE HIT (data still in cache)
sum = b[5] // CACHE HIT (data still in cache)
sum = a[6] // CACHE HIT (data still in cache)
sum = b[6] // CACHE HIT (data still in cache)
sum = a[7] // CACHE HIT (data still in cache)
sum = b[7] // CACHE HIT (data still in cache)
sum = a[8] // CACHE MISS
sum = b[8] // CACHE MISS
對于程式 2,它可能(請參閱注釋)以不同的順序出現相同數量的快取未命中:
sum = a[0] // CACHE MISS
sum = a[1] // CACHE HIT (data still in cache)
sum = a[2] // CACHE HIT (data still in cache)
sum = a[3] // CACHE HIT (data still in cache)
sum = a[4] // CACHE HIT (data still in cache)
sum = a[5] // CACHE HIT (data still in cache)
sum = a[6] // CACHE HIT (data still in cache)
sum = a[7] // CACHE HIT (data still in cache)
sum = a[8] // CACHE MISS
..然后:
sum = b[0] // CACHE MISS
sum = b[1] // CACHE HIT (data still in cache)
sum = b[2] // CACHE HIT (data still in cache)
sum = b[3] // CACHE HIT (data still in cache)
sum = b[4] // CACHE HIT (data still in cache)
sum = b[5] // CACHE HIT (data still in cache)
sum = b[6] // CACHE HIT (data still in cache)
sum = b[7] // CACHE HIT (data still in cache)
sum = b[8] // CACHE MISS
注意:我假設任何陣列都大于快取。如果快取足夠大以容納整個陣列但太小而無法容納兩個陣列;那么程式 2 可能會比程式 1 快。這是程式 2 更快的唯一情況。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/372502.html
