这段代码可以优化吗?

我有一些图像处理代码循环通过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-5ms。 虽然它是不安全的代码。

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

要获得此代码的任何实际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--; } } } 

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

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

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

一种选择是使用不安全的代码:将数组固定在内存中并使用指针操作。 我怀疑速度增加会如此戏剧化。

一个注意事项:你如何计时? 如果您正在使用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)); } } } 

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

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

一个例子(伪代码)可能是这样的:

 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

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

实际上,您可以通过反向循环并与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

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

编译器将进行优化

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

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

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

尝试将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)); } } 

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

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

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

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

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。

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

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

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

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

当然,如果您的图像数据分辨率太高(并且数字太大),这可能不实用。