我有一些图像处理代码循环通过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--;
}
}
}
答案 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速度交换内存空间,但它可以为你工作。
当然,如果您的图像数据分辨率过高(并且数字过于显着),则可能不实用。