我意识到这是一个愚蠢的问题,因为缺乏一个更好的术语.我只是在寻找任何有关提高此代码效率的外部想法,因为它会严重阻碍系统(它必须执行此功能)并且我的想法很少.
它正在做什么加载两个图像容器(imgRGB用于全彩色img和imgBW用于b&w图像)逐个像素的图像存储在"unsigned char*pImage"中.
imgRGB和imgBW都是用于根据需要访问各个像素的容器.
// input is in the form of an unsigned char // unsigned char *pImage for (int y=0; y < 640; y++) { for (int x=0; x < 480; x++) { imgRGB[y][x].blue = *pImage; pImage++; imgRGB[y][x].green = *pImage; imgBW[y][x] = *pImage; pImage++; imgRGB[y][x].red = *pImage; pImage++; } }
就像我说的那样,我只是在寻找有关更好的内存管理和/或复制的新输入和想法.有时候我会看到自己的代码,以至于我得到隧道视觉......有点心理障碍.如果有人想要/需要更多信息,请务必告诉我.
显而易见的问题是,您是否需要首先复制数据?你不能只定义访问器函数来从原始输入数组中提取任何给定像素的R,G和B值吗?
如果图像数据是瞬态的,那么您必须保留它的副本,您可以在不重新格式化的情况下制作它的原始副本,并再次定义访问器以索引到其上的每个像素/通道.
假设您提供的副本是必要的,将循环展开几次可能会有所帮助.
我认为最好的方法是将循环展开足够多次以确保每次迭代处理一个可被4字节整除的数据块(因此在每次迭代中,循环可以简单地读取少量的整数,而不是大量的chars)当然这要求你在写入时屏蔽这些int的位,但这是一个快速的操作,最重要的是,它是在寄存器中完成的,而不会增加内存子系统或CPU缓存的负担:
// First, we need to treat the input image as an array of ints. This is a bit nasty and technically unportable, but you get the idea) unsigned int* img = reinterpret_cast(pImage); for (int y = 0; y < 640; ++y) { for (int x = 0; x < 480; x += 4) { // At the start of each iteration, read 3 ints. That's 12 bytes, enough to write exactly 4 pixels. unsigned int i0 = *img; unsigned int i1 = *(img+1); unsigned int i2 = *(img+2); img += 3; // This probably won't make a difference, but keeping a reference to the found pixel saves some typing, and it may assist the compiler in avoiding aliasing. ImgRGB& pix0 = imgRGB[y][x]; pix0.blue = i0 & 0xff; pix0.green = (i0 >> 8) & 0xff; pix0.red = (i0 >> 16) & 0xff; imgBW[y][x] = (i0 >> 8) & 0xff; ImgRGB& pix1 = imgRGB[y][x+1]; pix1.blue = (i0 >> 24) & 0xff; pix1.green = i1 & 0xff; pix1.red = (i0 >> 8) & 0xff; imgBW[y][x+1] = i1 & 0xff; ImgRGB& pix2 = imgRGB[y][x+2]; pix2.blue = (i1 >> 16) & 0xff; pix2.green = (i1 >> 24) & 0xff; pix2.red = i2 & 0xff; imgBW[y][x+2] = (i1 >> 24) & 0xff; ImgRGB& pix3 = imgRGB[y][x+3]; pix3.blue = (i2 >> 8) & 0xff; pix3.green = (i2 >> 16) & 0xff; pix3.red = (i2 >> 24) & 0xff; imgBW[y][x+3] = (i2 >> 16) & 0xff; } }
你最好还是填写一个临时的ImgRGB值,然后立刻将整个结构写入内存,这意味着第一个块看起来就像这样:(当然下面的块类似)
ImgRGB& pix0 = imgRGB[y][x]; ImgRGB tmpPix0; tmpPix0.blue = i0 & 0xff; tmpPix0.green = (i0 >> 8) & 0xff; tmpPix0.red = (i0 >> 16) & 0xff; imgBW[y][x] = (i0 >> 8) & 0xff; pix0 = tmpPix0;
根据编译器的聪明程度,这可能会大大减少所需的读取次数.假设原始代码是天真编译的(这可能不太可能,但会作为一个例子),这将使您从每个像素3次读取和4次写入(读取RGB通道,并写入RGB + BW)到每个3/4读取像素和2写.(一个写入RGB结构,一个写入BW值)
您还可以在单个int中累积4个写入BW图像的写入,然后一次性写入,如下所示:
bw |= (i0 >> 8) & 0xff; bw |= (i1 & 0xff) << 8; bw |= ((i1 >> 24) & 0xff) << 16; bw |= ((i2 >> 16) & 0xff) << 24; *(imgBW + y*480+x/4) = bw; // Assuming you can treat imgBW as an array of integers
这将减少每像素1.25的写入次数(每个RGB结构1个,每4个BW值1个)
同样,好处可能会小很多(甚至不存在),但它可能值得一试.
更进一步,使用SSE指令可以毫不费力地完成同样的操作,允许每次迭代处理4倍的值.(假设你在x86上运行)
当然,这里一个重要的声明是,上面是不可移植.reinterpret_cast可能是一个学术点(无论如何都很有效,特别是如果你能确保原始数组在32位边界上对齐,这通常适用于所有平台上的大量分配)更大的问题是,苦涩取决于CPU的字节顺序.
但实际上,这应该适用于x86.并且只需稍加改动,它也适用于大端机器.(当然,我的代码中的任何错误都是模数.我没有测试过,甚至没有编译过任何错误;))
但无论你如何解决它,你都会看到最大限度地提高速度,最大限度地减少读写次数,并尽量在CPU寄存器中累积尽可能多的数据.尽可能读取大块中的所有内容,例如整数,在寄存器中重新排序(将其累积为多个整数,或将其写入RGB结构的临时实例),然后将这些组合值写入内存.
根据您对低级优化的了解程度,您可能会感到惊讶,但临时变量很好,而直接内存到内存访问可能很慢(例如,您的指针解除引用直接分配到数组中).这样做的问题是你可能会获得比必要更多的内存访问,并且编译器很难保证不会出现别名,因此它可能无法重新排序或组合内存访问.你通常可以在早期(循环的顶部)尽可能多地编写代码,尽可能多地在临时代码中执行(因为编译器可以将所有内容保存在寄存器中),然后在最后编写所有内容.这也为编译器提供了尽可能多的余地来等待最初的慢速读取.
最后,将第四个虚拟值添加到RGB结构(因此它的总大小为32位)很可能也会有很大帮助(因为编写这样的结构只需要一个32位写入,这比单个32位写入更简单,更有效.目前的24位)
在决定展开循环的程度时(每次迭代可以执行上述两次或更多次),请记住CPU有多少个寄存器.由于存在大量内存访问,因此扩散到缓存中可能会对您造成伤害,但另一方面,考虑到可用寄存器的数量,请尽可能多地展开(上面使用3个寄存器来保存输入数据,以及一个可以累积BW值.可能需要一两个来计算必要的地址,所以在x86上,上面加倍可能会推动它(你总共有8个寄存器,其中一些有特殊含义).另一方面,现代CPU通过在幕后使用更多的寄存器来做很多事情以补偿寄存器压力,因此进一步展开可能仍然是总体性能获胜.
一如既往,衡量措施措施.在你测试它之前,不可能说出什么是快速的,什么是不可能的.
要记住的另一个一般要点是数据依赖性很差.只要您只处理整数值,这仍然不是什么大问题,但它仍然会禁止指令重新排序和超标量执行.在上面,我试图尽可能缩短依赖链.不是不断地递增相同的指针(这意味着每个增量依赖于前一个增量),向同一个基地址添加不同的偏移意味着每个地址都可以独立计算,再次为编译器提供更多的自由来重新排序和重新安排说明.