这段代码能够进行优化吗?

7

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

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

循环目前需要约11毫秒,我认为这主要是由于访问字节数组值所致,因为计算非常简单(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--;
        }
    }
}

@Andrew Arnott:虽然完全正确,但也完全没用。 ;) - Guffa
能否更新一下采纳答案中代码的计时?知道在每个循环迭代中保存3个计数器的差异有多大会很有趣。 - Peter Mortensen
如果您查看我的问题的“UPDATE”部分,它就在那里。基于被接受答案的代码每个循环需要6-7毫秒,相比原始代码的约11毫秒。这有帮助吗?或者您是在询问代码的其他版本? - Matt Warren
14个回答

5
这些都是独立的计算,如果你有多核 CPU,可以并行计算以获得一些好处。请注意,您需要保留线程并仅交给它们要执行的工作,因为每次重新创建线程的开销可能会使其变慢而不是更快。
另一个可能有效的方法是将工作分配给图形处理器。例如,参考这个问题,使用Accelerator等工具可以提供一些想法。

5
为了让这段代码真正加速,您需要使用指针来访问数组,这将消除所有索引计算和边界检查。
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--;
      }
   }
}

编辑:
修复了变量无法更改的问题,所以我添加了代码将指针复制到新的可以更改的指针中。


如果AlphaValue和OneMinusAlphaValue是浮点数,使用定点数学可能会进一步提高速度。从浮点数到整数的转换可能会出乎意料地昂贵。 - Bids
当我尝试运行这段代码时,出现以下错误: 无法分配给'r',因为它是一个'固定变量' 无法分配给'c',因为它是一个'固定变量' 无法分配给'a',因为它是一个'固定变量' 我做错了什么吗?(我已经在我的项目中添加了“/unsafe”标志) - Matt Warren
没关系,我已经修好了,可以在我编辑过的问题中看到更新后的代码。谢谢你的帮助,你的方法快了4-5毫秒,这可是有很大的差别呢。 - Matt Warren
我明白了,你不能改变固定变量,所以必须将它们复制到指针中。我会更新代码。 - Guffa

4

一种选择是使用不安全的代码:将数组固定在内存中并使用指针操作。虽然我怀疑速度增加不会那么显著。

有一个注意点:你是如何计时的?如果你使用DateTime类,要注意这个类的分辨率很低。你应该添加一个外部循环并重复操作十次——我打赌结果会小于110毫秒。

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

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


PLINQ可能是一个合适的选择:http://msdn.microsoft.com/zh-cn/magazine/cc163329.aspx - Michael Meadows
肯定 schedule 方法不应该阻塞,只需将工作项添加到池的工作队列中即可。否则,您会添加n个项目,阻塞直到它们全部完成,再添加另外n个,依此类推。可以想象,池队列可能具有错误阈值,但这应该比n大得多。 - Paul Ruane
@Paul:你说得完全正确,我会编辑以提供更现实的用法。 - Evan Teran
使用线程池来处理明显适合于工作窃取框架(如PLinq或Task Parallel Lib)或OpenMP的工作是一个非常糟糕的想法。如果系统繁忙或只有一个处理器可用,线程池的工作速度会明显变慢。 - codekaizen
你可以始终获取CPU的数量来选择最佳线程数。此外,OpenMP在底层使用pthread,其性能很可能与正确使用的线程池类似。 - Evan Teran

3

我建议您运行一些空测试以确定您的理论界限。例如,从循环中删除计算并查看节省了多少时间。尝试用一个运行相同次数的单个循环替换双重循环,并查看它节省了多少时间。然后您就可以确信正在走向优化的正确道路(我看到的两条路是将双重循环展平为单个循环和使用乘法[也许使用查找表会更快])。


3

简单来说,您可以通过反向循环并与0进行比较来获得优化。大多数CPU都有一个快速的操作符用于与0进行比较。

例如:

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了解如何优化递减操作。

挑剔一点:声明“xSize - 1”时为什么不直接设置它,避免重复计算几千次呢? - Dan Lew
完成。不妨保存这个操作。很好的发现 :-) - torial

3

你可能正在受到边界检查的困扰。正如Jon Skeet所说,与多维数组(即data[,] )相比,使用锯齿状数组(即data[][])会更快,尽管这可能看起来很奇怪。

编译器将进行优化。

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

通过消除每个元素的范围检查,实现性能优化。但这只是某种特殊情况,Getlength()方法不会有相同的效果。
出于同样的原因,缓存或提升Length属性(将其放入像xSize这样的变量中)过去也不是一个好的做法,尽管我无法证实在Framework 3.5中是否仍然如此。

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

我正要推荐那个。+1 - qwerty

1
如果你正在使用LockBits来获取图像缓冲区,那么你应该在外部循环中通过y轴,在内部循环中通过x轴,因为这是存储在内存中的方式(按行而不是按列)。我可以说11毫秒已经相当快了...

我认为这实际上不会起作用 - 数组正在与y作为“次要”坐标一起使用,因此无论源是什么,我相信在内存中它将是[0,0],[0,1],[0,2]等 - 这就是迭代的方式。 - Jon Skeet

1
图片数据必须存储在多维(矩形)数组中吗?如果您使用分散的数组,您可能会发现JIT有更多的优化可用(包括删除边界检查)。

Jon,有没有一种有效的方法可以将MD数组中的数据获取到jagged数组中?我发现在循环中,jagged数组比MD数组快大约1毫秒。但是循环遍历MD数组并将每个值复制到jagged数组中需要更长的时间。 - Matt Warren
同时,我无法将数据直接放入锯齿数组中,因为将数据传递到.NET的第三方库只提供将数据放入MD数组的选项。 - Matt Warren
好的,那么这个答案对你来说并没有帮助:( 我会保留它,以防其他人处于类似的情况但有锯齿数组的选项。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接