2011-01-24 61 views
7

今天,我使用查找表而不是if-else來讀取代碼來剪裁兩個相加的uint8值。該地圖是我在i={0...255}和255在i={256...511}。我不知道有多大的這一增益可能,並試圖找到它,使用gprof的,Lookup Table vs if-else

g++ -std=c++0x -pg perfLookup.cpp -O2 -o perfLookup && ./perfLookup && gprof perfLookup |less 

與附後的代碼。現在沒有-O2標誌,gprof說lookup()佔用45%,ifelse()佔用48%的執行時間。使用-O2雖然查找()爲56%,而ifelse()爲43%。但是這個基準是否正確?也許很多代碼已經被優化掉了,因爲dst永遠不會被讀取?

#include <iostream> 
#include <cstdint> 
#include <vector> 

void lookup(std::vector<uint8_t> src, int repeat) { 
    uint8_t lookup[511]; 
    for (int i = 0; i < 256; i++) { 
    lookup[i] = i; 
    } 
    for (int i = 256; i < 512; i++) { 
    lookup[i] = 255; 
    } 

    std::vector<uint8_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int i = 0; i < src.size(); i++) { 
     dst[i] = lookup[src[i]]; 
    } 
    } 

} 

void ifelse(std::vector<uint8_t> src, int repeat) { 
    std::vector<uint8_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int i = 0; i < src.size(); i++) { 
     dst[i] = (src[i] > 255) ? 255 : src[i]; 
    } 
    } 
} 

int main() 
{ 
    int n = 10000; 
    std::vector<uint8_t> src(n); 
    for (int i = 0; i < src.size(); i++) { 
    src[i] = rand() % 510; 
    } 

    lookup(src, 10000); 
    ifelse(src, 10000); 
} 

更新代碼:

#include <iostream> 
#include <cstdint> 
#include <cstring> 
#include <vector> 
#include <algorithm> 

// g++ -std=c++0x -pg perfLookup.cpp -O2 -o perfLookup && ./perfLookup && gprof perfLookup |less 

std::vector<uint16_t> lookup(std::vector<uint16_t> src, int repeat) { 
    uint16_t lookup[511]; 
    for (int i = 0; i < 256; i++) { 
    lookup[i] = i; 
    } 
    for (int i = 256; i < 511; i++) { 
    lookup[i] = 255; 
    } 

    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int k = 0; k < src.size(); k++) { 
     dst[k] = lookup[src[k]]; 
    } 
    } 

    return dst; 

} 

std::vector<uint16_t> ifelse(std::vector<uint16_t> src, int repeat) { 
    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    for (int k = 0; k < src.size(); k++) { 
     dst[k] = (src[k] > 255) ? 255 : src[k]; 
    } 
    } 
    return dst; 
} 

std::vector<uint16_t> copyv(std::vector<uint16_t> src, int repeat) { 
    std::vector<uint16_t> dst(src.size()); 
    for (int i = 0; i < repeat; i++) { 
    dst = src; 
    for (int k = 0; k < src.size(); k++) { 
     if (dst[k] > 255) { 
    dst[k] = 255; 
     } 
    } 
    } 
    return dst; 
} 

std::vector<uint16_t> copyC(std::vector<uint16_t> src, int repeat) 
{ 
    uint16_t* dst = (uint16_t *) malloc(sizeof(uint16_t) * src.size()); // Alloc array for dst 

    for (int i = 0; i < repeat; i++) { 
    std::memcpy(dst, &src[0], sizeof(uint16_t) * src.size()); // copy src into array 

    for (int k = 0; k < src.size(); k++) { 
     if ((dst[k] & 0xFF00) != 0) 
    dst[k] = 0x00FF; 
    } 
    } 

    free(dst); 
    return std::vector<uint16_t>(); 
} 

int main() 
{ 
    int n = 10000; 
    std::vector<uint16_t> src(n); 
    for (int i = 0; i < src.size(); i++) { 
    src[i] = rand() % 510; 
    } 
    std::vector<uint16_t> dst; 
    dst = lookup(src, 10000); 
    dst = ifelse(src, 10000); 
    dst = copyv(src, 10000); 
} 
+3

請注意,您要衡量的查找表的初始化的基準測試的一部分。通常你分別初始化一個查找表,不要在基準測試中包含它。 – 2011-01-24 15:29:39

+0

我不會將查找表的初始化包含到已測量函數中,因爲在程序執行過程中只能執行一次。 – 2011-01-24 15:30:24

+1

我將對代碼進行一些更改:使用`src`參數並就地執行裁剪 - 注意這已經是一個副本,而不是對原始的引用。從函數中返回該向量,否則編譯器可能會從函數中刪除所有代碼,因爲本地變量從不使用。在測試代​​碼之外創建並存儲查找表 - 避免添加不會影響結果的操作。 – 2011-01-24 15:54:48

回答

7

好吧,既然src被聲明爲std::vector<uint8_t>src[i]從未255,這是一個8位無符號整數可能的最高值。

因此,我的猜測是編譯器優化了檢查。剩下的只是樣板循環,所以基準沒有意義。

如果支票沒有意義(即檢查64而不是255),那麼'優化'的結果可能是高度機器相關的。分支預測可能(取決於輸入數據)在降低分支成本方面做得很好。另一方面,查找表需要(同樣取決於輸入數據)隨機存儲器訪問並破壞緩存...

+0

好點!將所有內容更改爲uint16_t後,ifelse()爲58%,lookup()爲42%。所以編譯器確實足夠聰明。 (在-O3,它甚至優化了兩個函數調用,或多或少。)關於構建查找表,在更改循環次數時分佈也保持穩定。我想知道這在「真實」系統(視頻編輯效果,以及其他很多其他緩存訪問)中看起來如何... – 2011-01-24 16:18:47

2

您也在測量查找表的初始化時間,而且這可能不會成爲你想成爲的人。如果表只在生產代碼中初始化一次,但多次使用,則不應測量初始化。

+0

同意,我會從lookup功能中初始化查找表。你甚至可以手動對它進行初始化並使其成爲常量(例如,const uint8_t lookup [] = {0,1,2,3 ...}) – GrahamS 2011-01-24 15:48:19

+0

我同意;在這種情況下,構建LUT的速度足夠快,但幾乎沒有任何影響(特別是在循環多次時)。 – 2011-01-24 16:23:37

7

除了東西亞歷山大已經表示:

查找表可以提高性能大幅。但是,這首先被創建查找表的時間抵消了。通常你會單獨對基準進行評估。

一個必須牢記的另一件事是,查找表需要的緩存空間,因此可能導致高速緩存未命中,如果它的大。如果有足夠的高速緩存未命中時,if方法會比查找表要快。

最後,gprof是很好的識別瓶頸。但我不會將它用於基準測試。改用定時功能。 gprof使用抽樣,嚴格來說,抽樣可以映射到時間消耗,但在這裏不太精確。

+0

感謝您的提示!關於定時功能,你在這裏想到了什麼?掛鐘時間?還是CPU週期? – 2011-01-24 16:24:54

+0

@Simon:一旦你做了足夠的迭代(即單個週期長度> 1 s)和足夠的實驗重複,字面無關緊要。通常,分辨率越好,結果越好。但是,所有其他因素總是會影響基準,所以不要太重視時鐘。 – 2011-01-24 16:55:52

3

lookup陣列的處理被打破。這條線:

uint8_t lookup[511]; 

是關閉的一個,你想lookup[512];因爲你似乎期待指數與511(它訪問第512個元素)。當然,正如亞歷山大指出,這一切都毫無意義,因爲反正意味着uint8_t你不能有高於255的任何一個指標。

因爲它是,這樣的代碼:

for (int i = 256; i < 512; i++) { 
    lookup[i] = 255; 
} 

意願索引超出範圍和寫入255或多或少隨機選擇的存儲器位置。

1

有時編譯器足夠聰明,可以優化簡單的性能分析測試。如果是這種情況,你有竅門,編譯器不進行優化。使用更大的重複值也可能會幫助您獲得更好的結果,或告訴您是否正在優化某些內容。

查找表可以比鏈接if/elseifs更快,但在這種情況下只有一個比較我不會有太大的區別。例如,如果您有10個,100個,1000個比較,則查找表通常應該贏。

2

這兩種方法看起來都很奇怪。你真的需要這種優化水平嗎? 如果是這樣,那麼我會質疑向量的使用,並考慮C數組!

「ifelse」方法似乎更明顯。我懷疑這是顯然比查找表更慢/更快,除非你打這個數十億次。

我個人可能只是克隆SRC矢量然後遍歷它和固定值(使用250這裏,是因爲255是沒有意義的指出):

std::vector<uint8_t> dst(src); 
for(std::vector<int>::size_type i = 0; i != v.size(); i++) 
{ 
    if (dst[i] > 250) dst[i] = 250; 
} 

根據克隆實際上是如何進行的並由編譯器進行優化(例如,它可以執行塊存儲器複製),那麼這實際上可能會稍微快一些。它肯定更整潔,更容易理解。

1

一個可能的骯髒的小℃溶液來(從我的頭頂和未經考驗的/未編譯的,所以可能包含錯誤):

std::vector<uint16_t> copyC(std::vector<uint16_t> src, int repeat) 
{ 
    uint16_t* dst = malloc(sizeof(unit16_t) * src.size()); // Alloc array for dst 

    for (int i = 0; i < repeat; i++) 
    { 
     memcpy(dst, &src[0], sizeof(unit16_t) * src.size()); // copy src into array 

     for (int k = 0; k < src.size(); k++) 
     { 
      if ((dst[k] & 0xFF00) != 0) 
       dst[k] = 0x00FF; 
     } 
    } 

    free(dst); 
} 

我很想看看如何進行比較。 (同樣,它可能取決於memcpy的實現,因爲只有大內存副本比逐字節副本效率更高時纔會更快)。

根據芯片的規格(即8位或16位寄存器大小),單字節訪問可能比雙字節快。如果是這樣的話,上面的代碼也可以被重寫,將dst當作一個unit8_t的數組。那麼它只會檢查每個第二個字節,如果它是非零,則將其設置爲0,並將後面的字節*設置爲0xFF。

(*或前一個字節,根據字節序)