有什么方法可以使这个相对简单(嵌套用于内存复制)C ++代码更高效?

时间:2009-02-10 15:06:25

标签: c++ optimization image-processing memory-management

我意识到这是一个愚蠢的问题,因为缺乏一个更好的术语。我只是在寻找任何有关提高此代码效率的外部想法,因为它会严重阻碍系统(它必须执行此功能)并且我的想法很少。

它正在做什么加载两个图像容器(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++;
    }
}

就像我说的那样,我只是在寻找有关更好的内存管理和/或复制的新输入和想法。有时候我会看到自己的代码,以至于我得到隧道视觉......有点心理障碍。如果有人想要/需要更多信息,请务必告诉我。

13 个答案:

答案 0 :(得分:7)

答案 1 :(得分:4)

我认为数组访问(它们是真正的数组访问还是operator []?)会杀了你。每一个代表一个乘法。

基本上,你想要这样的东西:

for (int y=0; y < height; y++) {
    unsigned char *destBgr = imgRgb.GetScanline(y); // inline methods are better
    unsigned char *destBW = imgBW.GetScanline(y);
    for (int x=0; x < width; x++) {
        *destBgr++ = *pImage++;
        *destBW++ = *destBgr++ = *pImage++; // do this in one shot - don't double deref
        *destBgr++ = *pImage++;
    }
}

这将为每条扫描线进行两次乘法运算。您的代码每个PIXEL执行4次乘法。

答案 2 :(得分:4)

在这种情况下我喜欢做的是进入调试器并逐步完成反汇编以查看它实际上在做什么(或让编译器生成汇编列表)。这可以为您提供很多关于效率低下的线索。他们往往不在你想的地方!

通过实施上述Assaf和David Lee建议的更改,您可以获得前后指令计数。这真的有助于我优化紧密的内环。

答案 3 :(得分:3)

你可以使用下标运算符[] []优化掉你正在做的一些指针运算,而是使用迭代器(即推进指针)。

答案 4 :(得分:3)

内存带宽是你的瓶颈。将所有数据传输到系统存储器和从系统存储器传输所需的理论最小时间。我写了一个小测试来比较OP的版本和一些简单的汇编程序,看看编译器有多好。我正在使用具有默认释放模式设置的VS2005。这是代码:

#include <windows.h>
#include <iostream>
using namespace std;

const int
c_width = 640,
c_height = 480;

typedef struct _RGBData
{
  unsigned char
    r,
    g,
    b;
    // I'm assuming there's no padding byte here
} RGBData;

//  similar to the code given
void SimpleTest
(
  unsigned char *src,
  RGBData *rgb,
  unsigned char *bw
)
{
  for (int y = 0 ; y < c_height ; ++y)
  {
    for (int x = 0 ; x < c_width ; ++x)
    {
      rgb [x + y * c_width].b = *src;
      src++;

      rgb [x + y * c_width].g = *src;
      bw [x + y * c_width] = *src;
      src++;

      rgb [x + y * c_width].r = *src;
      src++;
    }
  }
}

//  the assembler version
void ASM
(
  unsigned char *src,
  RGBData *rgb,
  unsigned char *bw
)
{
  const int
    count = 3 * c_width * c_height / 12;

  _asm
  {
    push ebp
    mov esi,src
    mov edi,bw
    mov ecx,count
    mov ebp,rgb
l1:
    mov eax,[esi]
    mov ebx,[esi+4]
    mov edx,[esi+8]
    mov [ebp],eax
    shl eax,16
    mov [ebp+4],ebx
    rol ebx,16
    mov [ebp+8],edx
    shr edx,24
    and eax,0xff000000
    and ebx,0x00ffff00
    and edx,0x000000ff
    or eax,ebx
    or eax,edx
    add esi,12
    bswap eax
    add ebp,12
    stosd
    loop l1
    pop ebp
  }
}

//  timing framework
LONGLONG TimeFunction
(
  void (*function) (unsigned char *src, RGBData *rgb, unsigned char *bw),
  char *description,
  unsigned char *src, 
  RGBData *rgb,
  unsigned char *bw
)
{
  LARGE_INTEGER
    start,
    end;

  cout << "Testing '" << description << "'...";
  memset (rgb, 0, sizeof *rgb * c_width * c_height);
  memset (bw, 0, c_width * c_height);

  QueryPerformanceCounter (&start);

  function (src, rgb, bw);

  QueryPerformanceCounter (&end);

  bool
    ok = true;

  unsigned char
    *bw_check = bw,
    i = 0;

  RGBData
    *rgb_check = rgb;

  for (int count = 0 ; count < c_width * c_height ; ++count)
  {
    if (bw_check [count] != i || rgb_check [count].r != i || rgb_check [count].g != i || rgb_check [count].b != i)
    {
      ok = false;
      break;
    }

    ++i;
  }

  cout << (end.QuadPart - start.QuadPart) << (ok ? " OK" : " Failed") << endl;
  return end.QuadPart - start.QuadPart;
}

int main
(
  int argc,
  char *argv []
)
{
  unsigned char
    *source_data = new unsigned char [c_width * c_height * 3];

  RGBData
    *rgb = new RGBData [c_width * c_height];

  unsigned char
    *bw = new unsigned char [c_width * c_height];

  int
    v = 0;

  for (unsigned char *dest = source_data ; dest < &source_data [c_width * c_height * 3] ; ++dest)
  {
    *dest = v++ / 3;
  }

  LONGLONG
    totals [2] = {0, 0};

  for (int i = 0 ; i < 10 ; ++i)
  {
    cout << "Iteration: " << i << endl;
    totals [0] += TimeFunction (SimpleTest, "Initial Copy", source_data, rgb, bw);
    totals [1] += TimeFunction (       ASM, "    ASM Copy", source_data, rgb, bw);
  }

  LARGE_INTEGER
    freq;

  QueryPerformanceFrequency (&freq);

  freq.QuadPart /= 100000;

  cout << totals [0] / freq.QuadPart << "ns" << endl;
  cout << totals [1] / freq.QuadPart << "ns" << endl;


  delete [] bw;
  delete [] rgb;
  delete [] source_data;

  return 0;
}

C和汇编程序之间的比例约为2.5:1,即C是汇编程序版本的2.5倍。

我刚刚注意到原始数据是以BGR顺序排列的。如果副本交换了B和R组件,那么它确实使汇编代码更复杂一些。但它也会使C代码更复杂。

理想情况下,您需要确定理论上的最短时间,并将其与您实际获得的时间进行比较。为此,您需要知道内存频率和内存类型以及CPU MMU的工作情况。

答案 5 :(得分:2)

您可以尝试使用简单的强制转换来获取RGB数据,然后重新计算灰度数据:

#pragma pack(1)
typedef unsigned char bw_t;
typedef struct {
    unsigned char blue;
    unsigned char green;
    unsigned char red;
} rgb_t;
#pragma pack(pop)

rgb_t *imageRGB = (rgb_t*)pImage;
bw_t *imageBW = (bw_t*)calloc(640*480, sizeof(bw_t));
// RGB(X,Y) = imageRGB[Y*480 + X]
// BW(X,Y) = imageBW[Y*480 + X]

for (int y = 0; y < 640; ++y)
{
   // try and pull some larger number of bytes from pImage (24 is arbitrary)
   // 24 / sizeof(rgb_t) = 8
   for (int x = 0; x < 480; x += 24)
   {
       imageBW[y*480 + x    ] = GRAYSCALE(imageRGB[y*480 + x    ]);
       imageBW[y*480 + x + 1] = GRAYSCALE(imageRGB[y*480 + x + 1]);
       imageBW[y*480 + x + 2] = GRAYSCALE(imageRGB[y*480 + x + 2]);
       imageBW[y*480 + x + 3] = GRAYSCALE(imageRGB[y*480 + x + 3]);
       imageBW[y*480 + x + 4] = GRAYSCALE(imageRGB[y*480 + x + 4]);
       imageBW[y*480 + x + 5] = GRAYSCALE(imageRGB[y*480 + x + 5]);
       imageBW[y*480 + x + 6] = GRAYSCALE(imageRGB[y*480 + x + 6]);
       imageBW[y*480 + x + 7] = GRAYSCALE(imageRGB[y*480 + x + 7]);
   }
}

答案 6 :(得分:2)

您可以采取几个步骤。结果在这个答案结束时。

首先,使用指针

const unsigned char *pImage;

RGB *rgbOut = imgRGB;
unsigned char *bwOut = imgBW;

for (int y=0; y < 640; ++y) {
    for (int x=0; x < 480; ++x) {
        rgbOut->blue = *pImage;
        ++pImage;

        unsigned char tmp = *pImage;  // Save to reduce amount of reads.
        rgbOut->green = tmp;
        *bwOut = tmp;
        ++pImage;

        rgbOut->red = *pImage;
        ++pImage;

        ++rgbOut;
        ++bwOut;
    }
}

如果imgRGBimgBW被声明为:

unsigned char imgBW[480][640];
RGB imgRGB[480][640];

您可以合并两个循环

const unsigned char *pImage;

RGB *rgbOut = imgRGB;
unsigned char *bwOut = imgBW;

for (int i=0; i < 640 * 480; ++i) {
    rgbOut->blue = *pImage;
    ++pImage;

    unsigned char tmp = *pImage;  // Save to reduce amount of reads.
    rgbOut->green = tmp;
    *bwOut = tmp;
    ++pImage;

    rgbOut->red = *pImage;
    ++pImage;

    ++rgbOut;
    ++bwOut;
}

您可以利用单词读取比四个char读取更快的事实。我们将为此使用辅助宏。请注意,此示例假定使用little-endian目标系统。

const unsigned char *pImage;

RGB *rgbOut = imgRGB;
unsigned char *bwOut = imgBW;

const uint32_t *curPixelGroup = pImage;

for (int i=0; i < 640 * 480; ++i) {
    uint64_t pixels = 0;

#define WRITE_PIXEL         \
    rgbOut->blue = pixels;  \
    pixels >>= 8;           \
                            \
    rgbOut->green = pixels; \
    *bwOut = pixels;        \
    pixels >>= 8;           \
                            \
    rgbOut->red = pixels;   \
    pixels >>= 8;           \
                            \
    ++rgbOut;               \
    ++bwOut;

#define READ_PIXEL(shift) \
    pixels |= (*curPixelGroup++) << (shift * 8);

    READ_PIXEL(0);  WRITE_PIXEL;
    READ_PIXEL(1);  WRITE_PIXEL;
    READ_PIXEL(2);  WRITE_PIXEL;
    READ_PIXEL(3);  WRITE_PIXEL;
    /* Remaining */ WRITE_PIXEL;

#undef COPY_PIXELS
}

(您的编译器可能会优化第一个or中的冗余READ_PIXEL操作。它还会优化移位,删除多余的<< 0。)


如果RGB的结构如此:

struct RGB {
     unsigned char blue, green, red;
};

您可以进一步优化,直接复制到结构,而不是通过其成员(redgreenblue)。这可以使用匿名结构(或转换,但这使得代码更麻烦,可能更容易出错)来完成。 (同样,这取决于小端系统等等):

union RGB {
    struct {
        unsigned char blue, green, red;
    };

    uint32_t rgb:24;  // Make sure it's a bitfield, otherwise the union will strech and ruin the ++ operator.
};

const unsigned char *pImage;

RGB *rgbOut = imgRGB;
unsigned char *bwOut = imgBW;

const uint32_t *curPixelGroup = pImage;

for (int i=0; i < 640 * 480; ++i) {
    uint64_t pixels = 0;

#define WRITE_PIXEL         \
    rgbOut->rgb = pixels;   \
    pixels >>= 8;           \
                            \
    *bwOut = pixels;        \
    pixels >>= 16;          \
                            \
    ++rgbOut;               \
    ++bwOut;

#define READ_PIXEL(shift) \
    pixels |= (*curPixelGroup++) << (shift * 8);

    READ_PIXEL(0);  WRITE_PIXEL;
    READ_PIXEL(1);  WRITE_PIXEL;
    READ_PIXEL(2);  WRITE_PIXEL;
    READ_PIXEL(3);  WRITE_PIXEL;
    /* Remaining */ WRITE_PIXEL;

#undef COPY_PIXELS
}

您可以像阅读时一样优化写入像素(以单词写入而不是24位)。事实上,这是一个非常好的主意,并将成为优化的下一步。但是,编码太累了。 =]


当然,你可以用汇编语言编写例程。然而,这使它不像现在那样便携。

答案 7 :(得分:1)

我现在假设以下情况,所以如果我的假设是错误的,请告诉我:

a)imgRGB是

类型的结构

    struct ImgRGB
    {
      unsigned char blue;
      unsigned char green;
      unsigned char red;
    };

或至少类似的东西。

b)imgBW看起来像这样:


    struct ImgBW
    {
       unsigned char BW;
    };

c)代码是单线程的

假设上述情况,我发现您的代码有几个问题:

  • 您将分配到BW部分正好位于其他容器的分配中间。如果您正在使用现代CPU,那么每次切换容器并且您正在查看重新加载或切换缓存行时,随着数据大小的增加,L1缓存会失效。如今,高速缓存针对线性访问进行了优化,因此来回跳跃并没有帮助。访问主内存要慢得多,因此会受到明显的性能影响。为了验证这是否是一个问题,暂时我将删除分配给imgBW并测量是否有明显的加速。
  • 数组访问没有帮助,它可能会稍微减慢代码速度,虽然一个体面的优化器应该照顾它。我可能会沿着这些线编写循环,但不会期望获得大的性能提升。也许只有几个百分点。

    for (int y=0; y blue = *pImage;
            ...
        }
    }
  • 为了保持一致性,我会从使用postfix改为前缀增量,但我不希望看到大的收益。
  • 如果你可以浪费一点存储(好吧,25%)你可能会从结构ImgRGB添加第四个虚拟无符号字符获得,前提是这会将结构的大小增加到int的大小。本机int通常访问速度最快,如果你正在查看一个完全没有填充int的字符结构,你可能会遇到各种有趣的访问问题,这些问题会导致代码明显减慢,因为编译器可能会必须生成额外的指令来提取无符号字符。再次尝试这个并测量结果 - 它可能会产生明显的差异或根本没有。同样,将结构成员的大小从unsigned char增加到unsigned int可能会浪费大量空间,但可能会加快代码速度。然而,只要pImage是指向unsigned char的指针,你只会消除一半的问题。

总而言之,您只需要使您的循环适合您的底层硬件,因此对于特定的优化技术,您可能需要了解您的硬件运行良好以及它的功能是什么。

答案 8 :(得分:1)

确保将pImage,imgRGB和imgBW标记为__restrict。 使用SSE并一次执行16个字节。

实际上你从那里做的事情看起来你可以使用一个简单的memcpy()将pImage复制到imgRGB(因为imgRGB是行主格式,显然与pImage的顺序相同)。您可以通过使用一系列SSE swizzle和store ops填写imgBW来打包绿色值,但这可能很麻烦,因为您需要一次处理(3 * 16 =)48个字节。

启动时,您确定pImage和输出数组都处于dcache状态吗?尝试使用预取提示提前获取128个字节并进行测量以查看是否可以改善效果。

编辑如果您不在x86上,请将“SSE”替换为适合您硬件的SIMD指令集。 (那是VMX,Altivec,SPU,VLIW,HLSL等)。

答案 9 :(得分:0)

如果可能的话,将其修改为更高级别,然后将位或指令修复!

  • 您可以将B&amp; W图像类专门用于引用彩色图像类的绿色通道(从而为每个像素保存一份副本)。如果你总是成对创建它们,你可能根本不需要天真的imgBW类。

  • 通过关注如何在imgRGB中存储数据,您可以从输入数据中一次复制三元组。更好的是,您可以复制整个内容,甚至只是存储引用(这也使之前的建议变得容易)。

如果你不控制这里所有的实现,你可能会被卡住,然后:

  • 最后的手段:展开循环(提示有人提到Duff的设备,或者只是让编译器为你做这个......),虽然我认为你不会看到太多改进......

答案 10 :(得分:0)

您似乎将每个像素定义为某种结构或对象。使用基本类型(比方说,int)可能会更快。正如其他人所提到的,编译器很可能使用指针增量来优化数组访问。如果编译不能为您执行此操作,则可以在使用array [] []时自行执行此操作以避免乘法运算。

由于每个像素只需要3个字节,因此可以将一个像素打包成一个int。通过这样做,您可以一次复制3个字节而不是逐个字节。唯一棘手的事情是当你想要读取像素的各个颜色分量时,你需要一些掩码和移位。这可能会比使用int节省的开销更多。

或者您可以分别为3个颜色组件使用3个int数组。但是,你需要更多的存储空间。

答案 11 :(得分:0)

这是一个非常小的,非常简单的优化:

您反复提到imageRGB [y] [x],并且可能需要在每一步重新计算。

相反,计算一次,看看是否有所改善:

Pixel* apixel;

for (int y=0; y < 640; y++) {
    for (int x=0; x < 480; x++) {
        apixel = &imgRGB[y][x];

        apixel->blue = *pImage;
        pImage++;

        apixel->green = *pImage;
        imgBW[y][x]   = *pImage;
        pImage++;

        apixel->red = *pImage;
        pImage++;
    }
}

答案 12 :(得分:0)

如果pImage已完全在内存中,为什么需要按摩数据?我的意思是,如果它已经是伪RGB格式,为什么你不能只编写一些内联例程/宏,可以按需吐出值而不是复制它?

如果重新排列像素数据对以后的操作很重要,请考虑块操作和/或缓存线优化。