簡體   English   中英

為 SIMD 分配內存對齊的緩沖區; |16 如何給出 16 的奇數倍,為什么會這樣?

[英]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 架構的細節:

  • Altivec 向量大小為 16 字節(128 位)
  • 緩存行大小為 128 字節

現在,我們舉一個例子, 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

注意偏移值並將它們與緩存行邊界進行比較。 為簡單起見,我們假設內存分配在緩存行邊界。 這並不重要,如下所示。

  • 緩沖區 1 從偏移量 0 開始,這是緩存行的開始。
  • 緩沖區 2 從緩存行邊界(偏移 2*128=256)后 16 個字節開始。
  • 緩沖區 3 從緩存行邊界(位於偏移 4*128=512 處)開始 32 個字節。
  • 緩沖區 4 從緩存行邊界(偏移 6*128=768)開始 48 個字節。

請注意距最近緩存行邊界的偏移量是如何增加 16 個字節的。 現在,如果我們假設每個緩沖區中的數據將在 16 字節塊中被訪問,在前向,在一個循環中,那么緩存行將以相當特定的順序從內存中獲取。 讓我們考慮循環的中間部分(因為在開始時 CPU 必須為每個緩沖區的開頭獲取緩存行):

  • 迭代 5
    • 從偏移量 5*16=80 處的緩沖區 1 加載,我們仍然使用在先前迭代中獲取的緩存行。
    • 從偏移量 352 處的緩沖區 2 加載,我們仍在使用先前迭代中獲取的緩存行。 緩存行邊界在偏移 256 處,我們在它的偏移 96 處。
    • 從偏移量 624 處的緩沖區 3 加載,我們仍在使用先前迭代中獲取的緩存行。 緩存行邊界位於偏移 512 處,我們位於其偏移 112 處。
    • 從偏移量 896 處的緩沖區 4 加載,我們到達一個新的緩存行邊界並從內存中獲取一個新的緩存行。
  • 迭代 6
    • 從偏移量 6*16=96 處的緩沖區 1 加載,我們仍然使用在先前迭代中獲取的緩存行。
    • 從偏移量 368 處的緩沖區 2 加載,我們仍然使用在先前迭代中獲取的緩存行。 緩存行邊界在偏移 256 處,我們在其偏移 112 處。
    • 從偏移量 640 處的緩沖區 3 加載,我們到達一個新的緩存行邊界並從內存中獲取一個新的緩存行。
    • 從偏移量 896 處的緩沖區 4 加載,我們仍在使用上次迭代時獲取的緩存行。 緩存線邊界在偏移 896 處,我們在它的偏移 16 處。
  • 迭代 7
    • 從偏移量 7*16=112 處的緩沖區 1 加載,我們仍然使用在先前迭代中獲取的緩存行。
    • 從偏移量 384 處的緩沖區 2 加載,我們到達一個新的緩存行邊界並從內存中獲取一個新的緩存行。
    • 從偏移量 656 處的緩沖區 3 加載,我們仍在使用上次迭代時獲取的緩存行。 緩存線邊界位於偏移 640,我們位於偏移 16。
    • 從偏移量 912 處的緩沖區 4 加載,我們仍在使用先前迭代中獲取的緩存行。 緩存行邊界位於偏移 896,我們位於偏移 32。
  • 迭代 8
    • 從偏移量 8*16=128 處的緩沖區 1 加載,我們到達一個新的緩存行邊界並從內存中獲取一個新的緩存行。
    • 從偏移量 400 處的緩沖區 2 加載,我們仍在使用先前迭代中獲取的緩存行。 緩存行邊界位於偏移 384,我們位於偏移 16。
    • 從偏移量 672 處的緩沖區 3 加載,我們仍在使用先前迭代中獲取的緩存行。 緩存行邊界在偏移 640 處,我們在其偏移 32 處。
    • 從偏移量 944 處的緩沖區 4 加載,我們仍在使用先前迭代中獲取的緩存行。 緩存行邊界位於偏移 896,我們位於偏移 48。

請注意,從內存中獲取新緩存行的順序不依賴於每次循環迭代中訪問緩沖區的順序。 此外,它不取決於整個內存分配是否與緩存線邊界對齊。 另請注意,如果緩沖區內容以相反的順序訪問,則緩存行將按正向順序獲取,但仍按順序進行。

這種有序的高速緩存行獲取可能有助於 CPU 中的硬件優先,因此,當執行下一次循環迭代時,所需的高速緩存行已經被預獲取。 沒有它,循環的每 8 次迭代都將需要 4 個新的緩存行,無論程序訪問緩沖區的順序如何,這可能被解釋為對內存的隨機訪問並妨礙預取器。 根據循環復雜性,這 4 個緩存行獲取可能不會被亂序執行模型隱藏並引入停頓。 當您每次迭代最多只獲取 1 個緩存行時,這種情況不太可能發生。

另一個可能的好處是避免地址別名 我不知道 Power 的緩存組織,但如果nBytes是頁面大小的倍數,一次使用多個緩沖區,當每個緩沖區頁面對齊時,可能會導致大量錯誤的依賴關系並妨礙存儲到加載的轉發. 雖然代碼會在nBytes是頁面大小的倍數的情況下進行調整,但別名可能不是主要問題。

  1. 我是否正確地認為上述函數僅基於 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])

造成這種情況的性能原因可能包括:

  • 避免 x86 Sandybridge-family ( https://www.agner.org/optimize/blog/read.php?i=142 ; and Agner Fog's microarch pdf ) 上的緩存庫沖突
  • 在一個循環中訪問比 L1 或 L2 緩存關聯性更多的數組時避免沖突未命中 如果一個數組必須被逐出以騰出空間來緩存另一個數組,則可能每整行發生一次,而不是一行內的每個 SIMD 向量發生一次。
  • 避免存儲的內存消歧錯誤依賴項(4k aliasng)。 例如, L1 內存帶寬:使用相差 4096+64 字節的地址時效率下降 50% x86 Intel CPU 僅查看存儲/加載地址的低 12 位,作為對加載是否與動態存儲重疊的快速第一次檢查。 4k 頁面內與加載具有相同偏移量的存儲有效地對其進行了別名,直到硬件發現它實際上沒有,但這會延遲加載。 如果 PPC 上的記憶消歧有類似的快速路徑,我不會感到驚訝。
  • Andrey 對驚人的緩存未命中的猜測:我喜歡這個想法,與現代高端 x86 和 Apple 的高性能相比,它在具有有限亂序執行窗口(並且可能有限的內存級並行性)的早期 PowerPC CPU 上更為重要-端ARM。 https://en.wikipedia.org/wiki/AltiVec#Implementations 它也可能有助於現代有序 ARM CPU(它也可能具有有限的內存級並行性)。 我敢肯定,一些 Apple 設備使用了有序 ARM,至少作為 big.LITTLE 設置的低功耗內核。

(當我說“避免”時,有時這只是“減少發生的可能性”。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM