我需要从ushort arrayB中具有相同长度的相应索引值中快速减去ushort arrayA中的每个值。
此外,如果差异为负,我需要存储零,而不是负差。
(确切地说,长度= 327680,因为我从另一张相同尺寸的图像中减去640x512图像)。
下面的代码目前需要大约20毫秒,如果可能的话,我想在〜5毫秒内将其降低。不安全的代码是可以的,但请提供一个例子,因为我不擅长编写不安全的代码。
谢谢!
public ushort[] Buffer { get; set; }
public void SubtractBackgroundFromBuffer(ushort[] backgroundBuffer)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
int bufferLength = Buffer.Length;
for (int index = 0; index < bufferLength; index++)
{
int difference = Buffer[index] - backgroundBuffer[index];
if (difference >= 0)
Buffer[index] = (ushort)difference;
else
Buffer[index] = 0;
}
Debug.WriteLine("SubtractBackgroundFromBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
}
更新:虽然它不是严格意义上的C#,但为了其他人的利益,我终于最终使用以下代码将C ++ CLR类库添加到我的解决方案中。它运行在~3.1ms。如果使用非托管C ++库,则运行时间约为2.2毫秒。由于时差很小,我决定使用托管库。
// SpeedCode.h
#pragma once
using namespace System;
namespace SpeedCode
{
public ref class SpeedClass
{
public:
static void SpeedSubtractBackgroundFromBuffer(array<UInt16> ^ buffer, array<UInt16> ^ backgroundBuffer, int bufferLength);
};
}
// SpeedCode.cpp
// This is the main DLL file.
#include "stdafx.h"
#include "SpeedCode.h"
namespace SpeedCode
{
void SpeedClass::SpeedSubtractBackgroundFromBuffer(array<UInt16> ^ buffer, array<UInt16> ^ backgroundBuffer, int bufferLength)
{
for (int index = 0; index < bufferLength; index++)
{
buffer[index] = (UInt16)((buffer[index] - backgroundBuffer[index]) * (buffer[index] > backgroundBuffer[index]));
}
}
}
然后我称之为:
public void SubtractBackgroundFromBuffer(ushort[] backgroundBuffer)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
SpeedCode.SpeedClass.SpeedSubtractBackgroundFromBuffer(Buffer, backgroundBuffer, Buffer.Length);
Debug.WriteLine("SubtractBackgroundFromBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
}
答案 0 :(得分:4)
一些基准。
SubtractBackgroundFromBuffer:
这是问题的原始方法。SubtractBackgroundFromBufferWithCalcOpt:
这是用TTat提高计算速度的原始方法。SubtractBackgroundFromBufferParallelFor:
来自Selman22答案的解决方案。SubtractBackgroundFromBufferBlockParallelFor:
我的回答。与3.类似,但会将处理分为4096个值。SubtractBackgroundFromBufferPartitionedParallelForEach:
杰夫的第一个答案。SubtractBackgroundFromBufferPartitionedParallelForEachHack:
杰夫的第二个答案。<强>更新强>
有趣的是,通过使用(如布鲁诺·科斯塔所建议的)SubtractBackgroundFromBufferBlockParallelFor
我可以获得小幅度的提升(~6%)
Buffer[i] = (ushort)Math.Max(difference, 0);
而不是
if (difference >= 0)
Buffer[i] = (ushort)difference;
else
Buffer[i] = 0;
<强>结果
请注意,这是每次运行1000次迭代的总时间。
SubtractBackgroundFromBuffer(ms): 2,062.23
SubtractBackgroundFromBufferWithCalcOpt(ms): 2,245.42
SubtractBackgroundFromBufferParallelFor(ms): 4,021.58
SubtractBackgroundFromBufferBlockParallelFor(ms): 769.74
SubtractBackgroundFromBufferPartitionedParallelForEach(ms): 827.48
SubtractBackgroundFromBufferPartitionedParallelForEachHack(ms): 539.60
从这些结果看来,最佳方法将计算优化与小增益相结合,使用Parallel.For
对图像块进行操作。您的里程当然会有所不同,并行代码的性能对您运行的CPU很敏感。
测试工具
我在发布模式下为每个方法运行了这个。我这样开始并停止Stopwatch
以确保只测量处理时间。
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
ushort[] bgImg = GenerateRandomBuffer(327680, 818687447);
for (int i = 0; i < 1000; i++)
{
Buffer = GenerateRandomBuffer(327680, 128011992);
sw.Start();
SubtractBackgroundFromBuffer(bgImg);
sw.Stop();
}
Console.WriteLine("SubtractBackgroundFromBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
public static ushort[] GenerateRandomBuffer(int size, int randomSeed)
{
ushort[] buffer = new ushort[327680];
Random random = new Random(randomSeed);
for (int i = 0; i < size; i++)
{
buffer[i] = (ushort)random.Next(ushort.MinValue, ushort.MaxValue);
}
return buffer;
}
方法
public static void SubtractBackgroundFromBuffer(ushort[] backgroundBuffer)
{
int bufferLength = Buffer.Length;
for (int index = 0; index < bufferLength; index++)
{
int difference = Buffer[index] - backgroundBuffer[index];
if (difference >= 0)
Buffer[index] = (ushort)difference;
else
Buffer[index] = 0;
}
}
public static void SubtractBackgroundFromBufferWithCalcOpt(ushort[] backgroundBuffer)
{
int bufferLength = Buffer.Length;
for (int index = 0; index < bufferLength; index++)
{
if (Buffer[index] < backgroundBuffer[index])
{
Buffer[index] = 0;
}
else
{
Buffer[index] -= backgroundBuffer[index];
}
}
}
public static void SubtractBackgroundFromBufferParallelFor(ushort[] backgroundBuffer)
{
Parallel.For(0, Buffer.Length, (i) =>
{
int difference = Buffer[i] - backgroundBuffer[i];
if (difference >= 0)
Buffer[i] = (ushort)difference;
else
Buffer[i] = 0;
});
}
public static void SubtractBackgroundFromBufferBlockParallelFor(ushort[] backgroundBuffer)
{
int blockSize = 4096;
Parallel.For(0, (int)Math.Ceiling(Buffer.Length / (double)blockSize), (j) =>
{
for (int i = j * blockSize; i < (j + 1) * blockSize; i++)
{
int difference = Buffer[i] - backgroundBuffer[i];
Buffer[i] = (ushort)Math.Max(difference, 0);
}
});
}
public static void SubtractBackgroundFromBufferPartitionedParallelForEach(ushort[] backgroundBuffer)
{
Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
{
for (int i = range.Item1; i < range.Item2; ++i)
{
if (Buffer[i] < backgroundBuffer[i])
{
Buffer[i] = 0;
}
else
{
Buffer[i] -= backgroundBuffer[i];
}
}
});
}
public static void SubtractBackgroundFromBufferPartitionedParallelForEachHack(ushort[] backgroundBuffer)
{
Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
{
for (int i = range.Item1; i < range.Item2; ++i)
{
unsafe
{
var nonNegative = Buffer[i] > backgroundBuffer[i];
Buffer[i] = (ushort)((Buffer[i] - backgroundBuffer[i]) *
*((int*)(&nonNegative)));
}
}
});
}
答案 1 :(得分:4)
这是一个有趣的问题。
仅在测试结果不是负数后执行减法(由TTat和Maximum Cookie建议)影响可以忽略不计,因为这种优化已经可以由JIT编译器执行。
并行化任务(由Selman22建议)是一个好主意,但是当循环速度与此情况一样快时,开销最终会超过收益,因此实际Selman22's implementation我的测试运行速度较慢。我怀疑nick_w's benchmarks是在附加调试器的情况下生成的,隐藏了这个事实。
将任务放在较大的块中(如nick_w所示)处理开销问题,实际上可以产生更快的性能,但是你不必自己计算块 - 你可以使用{{3为你这样做:
public static void SubtractBackgroundFromBufferPartitionedParallelForEach(
ushort[] backgroundBuffer)
{
Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
{
for (int i = range.Item1; i < range.Item2; ++i)
{
if (Buffer[i] < backgroundBuffer[i])
{
Buffer[i] = 0;
}
else
{
Buffer[i] -= backgroundBuffer[i];
}
}
});
}
在我的测试中,上述方法始终优于Partitioner
手动分块。
但是等等!还有更多的东西。
减慢代码速度的真正原因不在于赋值或算术。这是if
声明。它如何影响性能将受到您正在处理的数据性质的重大影响。
nick_w's基准测试为两个缓冲区生成相同幅度的随机数据。但是,我怀疑你很可能在后台缓冲区中实际拥有较低的平均幅度数据。由于分支预测,此细节可能很重要(如nick_w's中所述)。
当后台缓冲区中的值通常小于缓冲区中的值时,JIT编译器会注意到这一点,并相应地优化该分支。当每个缓冲区中的数据来自相同的随机群体时,无法猜测if
语句的结果,准确度超过50%。正是后一种情况this classic SO answer正在进行基准测试,在这些条件下,我们可以通过使用不安全的代码将bool转换为整数并避免分支来进一步优化您的方法。 (请注意,以下代码依赖于bool如何在内存中表示的实现细节,虽然它适用于.NET 4.5中的场景,但它不一定是个好主意,并且在此处显示用于说明目的。)< / p>
public static void SubtractBackgroundFromBufferPartitionedParallelForEachHack(
ushort[] backgroundBuffer)
{
Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
{
for (int i = range.Item1; i < range.Item2; ++i)
{
unsafe
{
var nonNegative = Buffer[i] > backgroundBuffer[i];
Buffer[i] = (ushort)((Buffer[i] - backgroundBuffer[i]) *
*((int*)(&nonNegative)));
}
}
});
}
如果您真的希望减少更多时间,那么您可以通过将语言切换到C ++ / CLI以更安全的方式遵循此方法,因为这将允许您在算术表达式中使用布尔值而不诉诸于不安全代码:
UInt16 MyCppLib::Maths::SafeSubtraction(UInt16 minuend, UInt16 subtrahend)
{
return (UInt16)((minuend - subtrahend) * (minuend > subtrahend));
}
您可以使用C ++ / CLI创建一个纯托管的DLL,公开上面的静态方法,然后在C#代码中使用它:
public static void SubtractBackgroundFromBufferPartitionedParallelForEachCpp(
ushort[] backgroundBuffer)
{
Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
{
for (int i = range.Item1; i < range.Item2; ++i)
{
Buffer[i] =
MyCppLib.Maths.SafeSubtraction(Buffer[i], backgroundBuffer[i]);
}
});
}
这比上面的hacky不安全C#代码更胜一筹。实际上,它是如此之快以至于您可以使用C ++ / CLI编写整个方法而忘记并行化,并且它仍然会胜过其他技术。
使用nick_w,上述方法将胜过目前为止发布的任何其他建议。以下是我得到的结果(1-4是他试过的案例,5-7是这个答案中概述的案例):
1. SubtractBackgroundFromBuffer(ms): 2,021.37
2. SubtractBackgroundFromBufferWithCalcOpt(ms): 2,125.80
3. SubtractBackgroundFromBufferParallelFor(ms): 3,431.58
4. SubtractBackgroundFromBufferBlockParallelFor(ms): 1,401.36
5. SubtractBackgroundFromBufferPartitionedParallelForEach(ms): 1,197.76
6. SubtractBackgroundFromBufferPartitionedParallelForEachHack(ms): 742.72
7. SubtractBackgroundFromBufferPartitionedParallelForEachCpp(ms): 499.27
然而,在我希望你实际拥有的场景中,背景值通常较小,成功的分支预测可以全面改善结果,并且“黑客”可以避免if
声明实际上更慢:
当我将后台缓冲区中的值限制为范围0-6500
(c。缓冲区的10%)时,我使用nick_w's test harness获得的结果如下:
1. SubtractBackgroundFromBuffer(ms): 773.50
2. SubtractBackgroundFromBufferWithCalcOpt(ms): 915.91
3. SubtractBackgroundFromBufferParallelFor(ms): 2,458.36
4. SubtractBackgroundFromBufferBlockParallelFor(ms): 663.76
5. SubtractBackgroundFromBufferPartitionedParallelForEach(ms): 658.05
6. SubtractBackgroundFromBufferPartitionedParallelForEachHack(ms): 762.11
7. SubtractBackgroundFromBufferPartitionedParallelForEachCpp(ms): 494.12
您可以看到结果1-5已经大大改善,因为它们现在受益于更好的分支预测。结果6&amp; 7没有太大变化,因为他们避免了分支。
这种数据变化彻底改变了一切。在这种情况下,即使是最快的所有C#解决方案现在也只比原始代码快15%。
底线:请务必使用代表性数据测试您选择的任何方法,否则您的结果将毫无意义。
答案 2 :(得分:1)
您可以尝试Parallel.For
:
Parallel.For(0, Buffer.Length, (i) =>
{
int difference = Buffer[i] - backgroundBuffer[i];
if (difference >= 0)
Buffer[i] = (ushort) difference;
else
Buffer[i] = 0;
});
更新:我已经尝试过了,我看到你的情况有一个微小的差别,但是当阵列变大时,差异也变大了
答案 3 :(得分:1)
在实际执行减法之前,首先检查结果是否为负数,可能会略微提高性能。这样,如果结果为负,则不需要执行减法。例如:
if (Buffer[index] > backgroundBuffer[index])
Buffer[index] = (ushort)(Buffer[index] - backgroundBuffer[index]);
else
Buffer[index] = 0;
答案 4 :(得分:0)
以下是使用Zip()
的解决方案:
Buffer = Buffer.Zip<ushort, ushort, ushort>(backgroundBuffer, (x, y) =>
{
return (ushort)Math.Max(0, x - y);
}).ToArray();
它的表现不如其他答案,但它绝对是最短的解决方案。
答案 5 :(得分:0)
怎么样,
Enumerable.Range(0, Buffer.Length).AsParalell().ForAll(i =>
{
unsafe
{
var nonNegative = Buffer[i] > backgroundBuffer[i];
Buffer[i] = (ushort)((Buffer[i] - backgroundBuffer[i]) *
*((int*)(&nonNegative)));
}
});