这段代码可以优化吗?

时间:2009-03-31 17:41:18

标签: c# .net optimization image-processing

我有一些图像处理代码循环通过2个多维字节数组(大小相同)。它从源数组中获取一个值,对其执行计算,然后将结果存储在另一个数组中。

int xSize = ResultImageData.GetLength(0);
int ySize = ResultImageData.GetLength(1);

for (int x = 0; x < xSize; x++)
{                
   for (int y = 0; y < ySize; y++) 
   {                                                
      ResultImageData[x, y] = (byte)((CurrentImageData[x, y] * AlphaValue) +
                                    (AlphaImageData[x, y] * OneMinusAlphaValue));
   }
}

循环当前需要~11ms,我假设主要是因为访问字节数组值,因为计算非常简单(2次乘法和1次加法)。

我有什么办法可以加快速度吗?它是我的程序的一个时间关键部分,这个代码每秒被调用80-100次,所以任何速度增加,无论多么小,都会产生影响。此刻xSize = 768和ySize = 576,但将来会增加。

更新:感谢Guffa(请参阅下面的答案),以下代码为每个循环节省了4-5毫秒。虽然它是不安全的代码。

int size = ResultImageData.Length;
int counter = 0;
unsafe
{
    fixed (byte* r = ResultImageData, c = CurrentImageData, a = AlphaImageData)
    {
        while (size > 0)
        {
            *(r + counter) = (byte)(*(c + counter) * AlphaValue + 
                                    *(a + counter) * OneMinusAlphaValue);
            counter++;
            size--;
        }
    }
}

14 个答案:

答案 0 :(得分:5)

这些都是独立的计算,所以如果你有一个多核CPU,你应该能够通过并行计算获得一些好处。请注意,您需要保留线程,并且只需要处理它们,因为如果每次重新创建线程,线程创建的开销可能会使速度变慢而不是更快。

另一件可能有用的工作就是将工作分配给图形处理器。请查看this question以获取一些想法,例如,使用Accelerator

答案 1 :(得分:5)

要获得此代码的任何实际speadup,您需要使用指针来访问数组,这将删除所有索引计算和边界检查。

int size = ResultImageData.Length;
unsafe 
{
   fixed(byte* rp = ResultImageData, cp = CurrentImageData, ap = AlphaImageData) 
   {
      byte* r = rp;
      byte* c = cp;
      byte* a = ap;
      while (size > 0) 
      {
         *r = (byte)(*c * AlphaValue + *a * OneMinusAlphaValue);
         r++;
         c++;
         a++;
         size--;
      }
   }
}

编辑:
固定变量无法更改,因此我添加了代码以将指针复制到可以更改的新指针。

答案 2 :(得分:4)

一个选项是使用不安全的代码:将数组修复到内存中并使用指针操作。我怀疑速度的提升会是那么引人注目。

一个注意事项:你怎么计时?如果您使用的是DateTime,请注意此类的分辨率较差。你应该添加一个外部循环并重复操作十次 - 我打赌结果小于110ms。

for (int outer = 0; outer < 10; ++outer)
{
    for (int x = 0; x < xSize; x++)
    {                
         for (int y = 0; y < ySize; y++) 
         {                                                
              ResultImageData[x, y] = (byte)((CurrentImageData[x, y] * AlphaValue) +
                                             (AlphaImageData[x, y] * OneMinusAlphaValue));
         }
    }
}

答案 3 :(得分:4)

由于出现,矩阵中的每个单元格的计算完全独立于其他单元格。您可能希望考虑让多个线程处理此问题。为了避免创建线程的成本,您可以拥有一个线程池。

如果矩阵具有足够的尺寸,则可能是非常好的速度增益。另一方面,如果它太小,它可能没有帮助(甚至伤害)。值得一试。

示例(伪代码)可能是这样的:

void process(int x, int y) {
    ResultImageData[x, y] = (byte)((CurrentImageData[x, y] * AlphaValue) +
        (AlphaImageData[x, y] * OneMinusAlphaValue));
}

ThreadPool pool(3); // 3 threads big

int xSize = ResultImageData.GetLength(0);
int ySize = ResultImageData.GetLength(1);

for (int x = 0; x < xSize; x++) {
     for (int y = 0; y < ySize; y++)  {
         pool.schedule(x, y);  // this will add all tasks to the pool's work queue
     }
}

pool.waitTilFinished(); // wait until all scheduled tasks are complete

编辑: Michael Meadows在评论中提到plinq可能是一个合适的选择:http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

答案 4 :(得分:3)

我建议运行一些空的测试来弄清楚你的理论界限是什么。例如,从循环内部取出计算,看看节省了多少时间。尝试使用运行相同次数的单个循环替换双循环,并查看节省的时间。然后你可以确定你正在沿着正确的路径进行优化(我看到的两条路径将双循环展平为一个循环并使用乘法[可能使用查找表会更快]。

答案 5 :(得分:3)

实际上,您可以通过反向循环并与0进行比较来获得优化。大多数CPU具有快速运算以便与0进行比较。

E.g。

int xSize = ResultImageData.GetLength(0) -1;
int ySize = ResultImageData.GetLength(1) -1; //minor optimization suggested by commenter

for (int x = xSize; x >= 0; --x)
{                
     for (int y = ySize; y >=0; --y) 
     {                                                
          ResultImageData[x, y] = (byte)((CurrentImageData[x, y] * AlphaValue) +
                                         (AlphaImageData[x, y] * OneMinusAlphaValue));
     }
}

请参阅http://dotnetperls.com/Content/Decrement-Optimization.aspx

答案 6 :(得分:3)

你可能患有Boundschecking。就像Jon Skeet所说的那样,一个锯齿状的数组而不是多维数(即data[][]而不是data[,])会更快,看起来很奇怪。

编译器将优化

for (int i = 0; i < data.Length; i++) 

通过消除每元素范围检查。但它是某种特殊情况,它对Getlength()不会做同样的事情。

出于同样的原因,缓存或提升Length属性(将它放在像xSize这样的变量中)也曾经是一件坏事,尽管我还没有用Framework 3.5来验证它

答案 7 :(得分:2)

尝试将x和y换成循环以获得更线性的内存访问模式,从而减少缓存未命中次数,如此。

int xSize = ResultImageData.GetLength(0);
int ySize = ResultImageData.GetLength(1);

for (int y = 0; y < ySize; y++) 
{
    for (int x = 0; x < xSize; x++)
    {
        ResultImageData[x, y] = (byte)((CurrentImageData[x, y] * AlphaValue) +
            (AlphaImageData[x, y] * OneMinusAlphaValue));
    }
}

答案 8 :(得分:1)

如果你使用LockBits来获取图像缓冲区,你应该在外部循环中循环y,在内部循环中循环x,因为它是如何存储在内存中的(按行而不是列)。我会说11ms非常快,但是......

答案 9 :(得分:1)

图像数据是否必须存储在多维(矩形)数组中?如果您使用锯齿状数组,您可能会发现JIT有更多可用的优化(包括删除边界检查)。

答案 10 :(得分:1)

如果每次运行代码段时CurrentImageData和/或AlphaImageData都不会更改,则可以在运行显示的代码段之前存储产品,并避免在循环中进行乘法运算。

编辑:我刚才想到的另一件事:有时int操作比字节操作更快。使用处理器缓存利用率抵消这一点(您将大大增加数据大小并且更有可能导致缓存未命中)。

答案 11 :(得分:1)

442,368次加法和884,736次乘法计算我觉得11ms在现代CPU上实际上非常慢。

虽然我对.net的细节知之甚少,但我知道高速计算不是它的强项。在过去,我已经构建了类似问题的java应用程序,我总是使用C库来进行图像/音频处理。

从硬件角度来看,您希望确保内存访问是顺序的,即按存储器中存在的顺序逐步执行缓冲区。您还可能需要对此进行重新排序,以便编译器利用可用指令(如SIMD)。如何处理这将最终依赖于您的编译器,我无法帮助vs.net。

在嵌入式DSP上我会爆发

(AlphaImageData [x,y] * OneMinusAlphaValue)和(CurrentImageData [x,y] * AlphaValue)并使用SIMD指令计算缓冲区,可能在执行添加之前并行计算。也许做足够小的块来将缓冲区保存在cpu上。

我相信你所做的任何事情都需要比.net允许更直接访问内存/ cpu。

答案 12 :(得分:1)

您可能还想查看Mono运行时及其Simd扩展。也许你的一些计算可以利用SSE加速,因为我收集你基本上做矢量计算(我不知道哪个矢量大小有加速乘法,但有一些大小)

(宣布Mono.Simd的博客文章:http://tirania.org/blog/archive/2008/Nov-03.html

当然,这不适用于Microsoft .NET,但您可能对某些实验感兴趣。

答案 13 :(得分:1)

有趣的是,图像数据通常非常相似,这意味着计算可能非常重复。你有没有探索过计算的查找表?所以任何时候0.8乘以128 - 值[80,128],你已经预先计算到102.4,你只是看了那个?你基本上是在为CPU速度交换内存空间,但它可以为你工作。

当然,如果您的图像数据分辨率过高(并且数字过于显着),则可能不实用。