[英]Allocating memory aligned buffers for SIMD; how does |16 give an odd multiple of 16, and why do it?
我正在研究一個 C++ 函數來在內存中分配多個緩沖區。 緩沖區必須是 N 字節對齊的,因為它們保存的數據將使用各種類型的 SIMD 指令集(SSE、AVX、AVX512 等...)進行處理
在 Apple Core Audio Utility Classes online 中,我發現了這段代碼:
void CABufferList::AllocateBuffers(UInt32 nBytes)
{
if (nBytes <= GetNumBytes()) return;
if (mABL.mNumberBuffers > 1) {
// align successive buffers for Altivec and to take alternating
// cache line hits by spacing them by odd multiples of 16
nBytes = ((nBytes + 15) & ~15) | 16;
}
UInt32 memorySize = nBytes * mABL.mNumberBuffers;
Byte *newMemory = new Byte[memorySize], *p = newMemory;
memset(newMemory, 0, memorySize); // get page faults now, not later
AudioBuffer *buf = mABL.mBuffers;
for (UInt32 i = mABL.mNumberBuffers; i--; ++buf) {
if (buf->mData != NULL && buf->mDataByteSize > 0) {
// preserve existing buffer contents
memcpy(p, buf->mData, buf->mDataByteSize);
}
buf->mDataByteSize = nBytes;
buf->mData = p;
p += nBytes;
}
Byte *oldMemory = mBufferMemory;
mBufferMemory = newMemory;
mBufferCapacity = nBytes;
delete[] oldMemory;
}
代碼非常簡單,但是有一行我沒有完全理解:
nBytes = ((nBytes + 15) & ~15) | 16;
我知道它將字節數對齊/量化為 16,但是我不明白為什么它最后使用按位 OR 16。 評論說:“通過以 16 的奇數倍間隔來交替緩存行命中”。 原諒我的厚度,但我還是不明白。
所以我有三個問題:
1)做什么| 16;
| 16;
完全做,為什么要這樣做?
2)考慮內存分配和數據訪問的上下文,如何以及用什么術語| 16;
| 16;
改進代碼? 從代碼中的注釋我可以猜測它與緩存訪問有關,但我不理解整個“交替緩存行命中”位。 將內存分配地址間隔為 16 的奇數倍如何改進緩存訪問?
3) 我認為上述函數只能基於 new 運算符將返回至少 16 字節對齊內存的假設正確工作嗎? 在 C++ 中,new 運算符被定義為返回一個指向存儲的指針,該指針的對齊適用於任何具有基本對齊要求的對象,可能不一定是 16 字節。
根據引用 Altivec 的評論,這是特定於我不熟悉的 Power 架構的。 另外,代碼不完整,但看起來分配的內存是組織在一個或多個相鄰的緩沖區中的,並且大小調整僅在存在多個緩沖區時才起作用。 我們不知道如何訪問這些緩沖區中的數據。 這個答案會有很多假設,以至於它可能完全不正確。 我發布它主要是因為它太大而無法發表評論。
我可以看到尺寸修改的一種可能優勢。 首先,讓我們記住一些關於 Power 架構的細節:
現在,我們舉一個例子, AllocateBuffers
為 4 個緩沖區分配內存(即mABL.mNumberBuffers
為 4), nBytes
為 256。讓我們看看這些緩沖區在內存中是如何布局的:
| Buffer 1: 256+16=272 bytes | Buffer 2: 272 bytes | Buffer 3: 272 bytes | Buffer 4: 272 bytes |
^ ^ ^ ^
| | | |
offset: 0 272 544 816
注意偏移值並將它們與緩存行邊界進行比較。 為簡單起見,我們假設內存分配在緩存行邊界。 這並不重要,如下所示。
請注意距最近緩存行邊界的偏移量是如何增加 16 個字節的。 現在,如果我們假設每個緩沖區中的數據將在 16 字節塊中被訪問,在前向,在一個循環中,那么緩存行將以相當特定的順序從內存中獲取。 讓我們考慮循環的中間部分(因為在開始時 CPU 必須為每個緩沖區的開頭獲取緩存行):
請注意,從內存中獲取新緩存行的順序不依賴於每次循環迭代中訪問緩沖區的順序。 此外,它不取決於整個內存分配是否與緩存線邊界對齊。 另請注意,如果緩沖區內容以相反的順序訪問,則緩存行將按正向順序獲取,但仍按順序進行。
這種有序的高速緩存行獲取可能有助於 CPU 中的硬件優先,因此,當執行下一次循環迭代時,所需的高速緩存行已經被預獲取。 沒有它,循環的每 8 次迭代都將需要 4 個新的緩存行,無論程序訪問緩沖區的順序如何,這可能被解釋為對內存的隨機訪問並妨礙預取器。 根據循環復雜性,這 4 個緩存行獲取可能不會被亂序執行模型隱藏並引入停頓。 當您每次迭代最多只獲取 1 個緩存行時,這種情況不太可能發生。
另一個可能的好處是避免地址別名。 我不知道 Power 的緩存組織,但如果nBytes
是頁面大小的倍數,一次使用多個緩沖區,當每個緩沖區頁面對齊時,可能會導致大量錯誤的依賴關系並妨礙存儲到加載的轉發. 雖然代碼會在nBytes
是頁面大小的倍數的情況下進行調整,但別名可能不是主要問題。
- 我是否正確地認為上述函數僅基於 new 運算符將返回至少 16 字節對齊內存的假設才能正常工作? 在 C++ 中,new 運算符被定義為返回一個指向存儲的指針,該指針的對齊適用於任何具有基本對齊要求的對象,可能不一定是 16 字節。
是的,C++ 不保證任何特定的對齊方式,只是它適合存儲任何基本類型的對象。 C++17 添加了對過度對齊類型的動態分配的支持。
但是,即使使用較舊的 C++ 版本,每個編譯器也遵守目標系統 ABI 規范,該規范可能指定內存分配的對齊方式。 實際上,在許多系統上malloc
返回至少 16 字節對齊的指針, operator new
使用malloc
或類似的低級 API 返回的malloc
。
雖然它不是便攜式的,因此不是推薦的做法。 如果您需要特定的對齊方式,請確保針對 C++17 進行編譯或使用專門的 API,例如posix_memalign
。
回復:“如何”部分:在一組位( 0x10
又名16
)中進行 ORing 使其成為16
的奇數倍。即使是 16 的倍數也會清除該位,即它們也是 32 的倍數。這確保不是案子。
例如: 32 | 16
32 | 16
= 48。48 48 | 16
48 | 16
= 48。同樣的事情適用,無論在對齊 16 后在值中設置其他高位。
請注意,這里調整的是分配大小。 因此,如果多個緩沖區從一個大分配中連續雕刻出來,它們將不會全部以相對於緩存線邊界的相同對齊方式開始。 正如安德烈的回答所指出的那樣,如果它們最終的大小為n * line_size + 16
,它們可能會錯開。
如果它們都被分配到頁面開頭對齊的緩沖區的開頭,分配器會回退到直接使用mmap
進行大分配(例如 glibc 的 malloc),那么它根本沒有幫助。 據推測(至少在撰寫本文時),Apple 沒有這樣做。
請求大小為 2 的大冪的緩沖區可能並不罕見。
請注意,此評論可能很舊:Altivec 是 Apple 的第一個帶有 SIMD 的 ISA,在他們采用 x86 之前,並且在他們使用 ARM + NEON 制造 iPhone 之前。
傾斜您的緩沖區(因此它們相對於頁面或緩存行並非全部對齊)在 x86 上仍然很有用,並且可能在 ARM 上也很有用。
這些緩沖區的用例必須包括在相同索引處訪問其中兩個或多個緩沖區的循環。 例如A[i] = f(B[i])
。
造成這種情況的性能原因可能包括:
(當我說“避免”時,有時這只是“減少發生的可能性”。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.