2011-01-24 52 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。

(*或前一个字节,根据字节序)