今天我使用查找表而不是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);
}
答案 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。
(*或前一个字节,取决于字节顺序)