查找表vs if-else

时间:2011-01-24 15:23:05

标签: c++ performance optimization

今天我使用查找表而不是if-else读取代码来剪切两个求和的uint8值。地图位于i={0...255}中,i={256...511}中为255。我想知道这可能有多大,并试图找出它,使用gprof,

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);
}

7 个答案:

答案 0 :(得分:7)

好吧,由于src被声明为std::vector<uint8_t>src[i] 永远不会大于255,这是一个最高可能值8位无符号整数。

因此,我的猜测是编译器优化了检查。剩下的只是样板循环,所以基准没有意义。

如果检查没有意义(即检查64而不是255),“优化”的结果可能与机器高度相关。分支预测可以(取决于输入数据)在降低分支成本方面做得很好。另一方面,查找表需要(再次取决于输入数据)随机存储器访问并破坏缓存......

答案 1 :(得分:7)

除了亚历山大已经说过的事情:

查找表可以显着提高性能 。但是,这首先会被创建查找表所花费的时间所抵消。通常你会单独对此进行基准测试。

必须记住的另一件事是查找表需要缓存中的空间,因此如果它很大,可能会导致缓存未命中。如果有足够的缓存未命中,if方法将比查找表更快。

最后,gprof非常适合识别瓶颈。但我不会将它用于基准测试。请改用计时功能。 gprof使用的采样严格来说可以映射到消耗的时间,但在这里不太精确。

答案 2 :(得分:3)

lookup数组的处理被破坏了。这一行:

uint8_t lookup[511];

是一个,你想要lookup[512];,因为你似乎期望用511索引(它访问第512个元素)。当然,正如亚历山大指出的那样,无论如何,这都是没有意义的,因为uint8_t意味着你不能拥有255以上的任何索引。

实际上,这段代码:

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

将索引越界,并将255写入或多或少随机选择的内存位置。

答案 3 :(得分:2)

您正在测量查找表初始化的时间,这可能不是您想要的。如果表只在生产代码中初始化一次,但多次使用,那么就不应该测量初始化。

答案 4 :(得分: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;
}

根据编译器实际执行和优化克隆的方式(例如,它可能执行块内存复制),这实际上可能稍微快一些。它当然更整洁,更容易理解。

答案 5 :(得分:1)

有时编译器足够聪明,可以优化简单的分析测试。如果是这种情况,你就有了编译器没有优化的技巧。使用更大的重复值也可以帮助您获得更好的结果,或者告诉您是否正在优化某些内容。

如果/ elseifs,查找表可以比链接更快,但在这种情况下只有一个比较,我不会期望太大的区别。例如,如果您有10次,100次,1000次......比较,则查找表通常应该获胜。

答案 6 :(得分:1)

一个可能很脏的小C解决方案(脱离我的头顶并且未经测试/未编译,因此可能包含错误):

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。

(*或前一个字节,取决于字节顺序)