我正在编写一个WPF应用程序,该应用程序处理来自IR摄像机的图像数据流。该应用程序使用一个类库来处理诸如重新缩放或着色之类的步骤,我也在写自己。图像处理步骤如下所示:
ProcessFrame(double[,] frame)
{
int width = frame.GetLength(1);
int height = frame.GetLength(0);
byte[,] result = new byte[height, width];
Parallel.For(0, height, row =>
{
for(var col = 0; col < width; ++col)
ManipulatePixel(frame[row, col]);
});
}
帧由在后台运行的任务处理。问题是,取决于特定处理算法(ManipulatePixel()
)的成本,应用程序无法再跟上相机的帧速率。但是,我注意到,尽管我使用并行for循环,但该应用程序根本不会使用所有可用的CPU-“任务管理器性能”选项卡显示约60-80%的CPU使用率。
我以前在C ++中使用了相同的处理算法,使用了并行模式库中的concurrency::parallel_for
循环。正如我期望的那样,C ++代码使用了它可以获得的所有CPU,并且我还尝试从C#代码中PInvoking
使用C ++ DLL,并执行在C#库中运行缓慢的相同算法-它还使用了所有可用的CPU功率,实际上整个时间的CPU使用率都在100%左右,跟上相机的需求也没有任何问题。
将代码外包到C ++ DLL中,然后将其编组回C#是一个额外的麻烦,我当然会避免。如何使我的C#代码实际利用所有CPU潜力?我试图像这样增加进程优先级:
using (Process process = Process.GetCurrentProcess())
process.PriorityClass = ProcessPriorityClass.RealTime;
有效果的,但是效果很小。我还尝试过为Parallel.For()
循环设置并行度,如下所示:
ParallelOptions parallelOptions = new ParallelOptions();
parallelOptions.MaxDegreeOfParallelism = Environment.ProcessorCount;
,然后将其传递到Parallel.For()
循环,这完全没有效果,但是我想这并不奇怪,因为默认设置应该已经优化。我还尝试在应用程序配置中设置此设置:
<runtime>
<Thread_UseAllCpuGroups enabled="true"></Thread_UseAllCpuGroups>
<GCCpuGroup enabled="true"></GCCpuGroup>
<gcServer enabled="true"></gcServer>
</runtime>
但这实际上使它运行得更慢。
编辑: 我最初引用的ProcessFrame代码块实际上不是很正确。当时我在做什么:
ProcessFrame(double[,] frame)
{
byte[,] result = new byte[frame.GetLength(0), frame.GetLength(1)];
Parallel.For(0, frame.GetLength(0), row =>
{
for(var col = 0; col < frame.GetLength(1); ++col)
ManipulatePixel(frame[row, col]);
});
}
对此很抱歉,我当时在解释代码,但我没有意识到这是一个实际的陷阱,会产生不同的结果。此后,我已将代码更改为最初编写的代码(即,在函数开始处设置的width和height变量,并且数组的length属性每个仅查询一次,而不是在for循环的条件语句中查询)。谢谢@Seabizkit,您的第二条评论启发了我尝试此操作。实际上,所做的更改已经使代码运行速度明显提高-我没有意识到这一点,因为C ++不知道2D数组,因此无论如何我都必须将像素尺寸作为单独的参数传递。它是否足够快我还不能说。
也感谢您提供其他答案,它们包含了许多我尚不知道的东西,但是很高兴知道我要寻找什么。我将在达到满意结果后进行更新。
答案 0 :(得分:4)
我需要拥有所有代码并能够在本地运行它才能诊断问题,因为您的发布没有详细信息(我需要查看ManipulatePixel
函数内部以及调用ProcessFrame
的代码)。但这是适用于您的情况的一些一般性提示。
.NET中的2D数组比1D数组和交错数组要慢得多,即使在当今的.NET Core中,这也是一个长期存在的错误。
stackalloc
管理缓冲区的生存期,并将该指针(unsafe
传递给线程委托。)在线程之间共享内存缓冲区使系统更加难以优化安全的内存访问。
考虑使用.NET中的SIMD和AVX功能。尽管现代的C / C ++编译器足够聪明,可以编译代码以使用这些指令,但是.NET JIT并不那么热-但是您可以对SMID/AVX instructions using the SIMD-enabled types进行显式调用(您需要使用.NET Core 2.0或更高版本以获得最佳加速功能)
也请避免在C#的for
循环内复制单个字节或标量值,而应考虑对大容量复制操作使用Buffer.BlockCopy
(因为这些可以使用硬件内存复制功能)。 / p>
关于“ 80%CPU使用率”的观察-如果程序中存在循环,则 会在操作所提供的时间段内导致100%CPU使用率,系统-如果看不到100%使用率,则代码如下:
Thread.Sleep
)阻塞。使用ETW之类的工具来查看您的进程在认为它应该受到CPU限制时在做什么。lock
(Monitor
)调用或其他线程或内存同步原语。答案 1 :(得分:2)
效率问题 (这不是不是 true- [PARALLEL]
,但可能但不一定要从中受益“公正” -[CONCURRENT]
的工作
内联程序集,根据CPU层次结构中的缓存行大小进行了优化,并保持索引遵循2D数据{ column-wise | row-wise }
的实际内存布局。鉴于没有提到2D内核转换,因此您的过程无需“触摸”任何拓扑邻居,索引可以以“跨越” 2D域和{{1} }在转换相当大的像素块时可能会更有效,而不是仅为每个孤立的原子化1px(ILP +缓存效率就在您身边)来承担调用进程的所有开销。
给您的目标生产平台CPU系列,AVX2提供的最佳使用(块SIMD)矢量化指令,最佳AVX512代码。如您所知,可以使用带有AVX-intrinsics的C / C ++通过程序集检查来优化性能,最后为C#程序集内联“复制”最佳结果程序集。没有任何东西会运行得更快。具有CPU核心亲和力映射和逐出/保留的技巧确实是最后的选择,但实际上确实可以帮助实现几乎硬实时生产设置(尽管很少获得硬R / T系统在具有不确定性行为的生态系统中开发)
测试并基准化将“更贵”的部分(ManipulatePixel()
内的Parallel.For(...{...})
移到相反位置)的每批帧的运行时间,以查看成本的变化for(var col = 0; col < width; ++col){...}
工具的实例化。
接下来,如果采用这种廉价的方式,请考虑重构Parallel.For()
以至少使用一个数据块,并与数据存储布局对齐,并将其作为缓存行长度的倍数(对于缓存-命中 ManipulatePixel()
,提高了内存访问成本, ~ 0.5 ~ 5 [ns]
否则-在这里,是分发作品的意愿(每1像素更差)由于扩展了跨NUMA(非本地)内存地址的访问延迟,并且除了从不重新使用昂贵的高速缓存的提取数据块之外,在所有NUMA-CPU内核上的访问都将花费更多的时间,您故意从跨NUMA(非本地)内存获取中支付了过多的费用(从中您仅使用1px并“扔掉”了其余所有缓存块(因为这些像素将被重新获取和处理)在其他时间使用其他CPU内核〜浪费了三倍的时间〜很抱歉明确提到了这一点,但是在剃除每种可能的~ 100 ~ 380 [ns]
时,这不可能在生产管道中发生))
无论如何,我希望您的毅力和前进的步伐能使您获得所需的效率,回到您的身边。
答案 2 :(得分:0)
这是我最后要做的,主要是根据戴的回答:
我也尝试使用1D数组而不是2D来获得成功,但实际上没有任何区别。我不知道这是否是因为Dai提到的错误已同时修复,但我无法确定2D数组的速度是否比1D数组慢。
也许也值得一提,我最初的帖子中的ManipulatePixel()函数实际上更多是一个占位符,而不是对另一个函数的真正调用。这是我对框架所做的更恰当的示例,包括所做的更改:
private static void Rescale(ushort[,] originalImg, byte[,] scaledImg, in (ushort, ushort) limits)
{
Debug.Assert(originalImg != null);
Debug.Assert(originalImg.Length != 0);
Debug.Assert(scaledImg != null);
Debug.Assert(scaledImg.Length == originalImg.Length);
ushort min = limits.Item1;
ushort max = limits.Item2;
int width = originalImg.GetLength(1);
int height = originalImg.GetLength(0);
Parallel.For(0, height, row =>
{
for (var col = 0; col < width; ++col)
{
ushort value = originalImg[row, col];
if (value < min)
scaledImg[row, col] = 0;
else if (value > max)
scaledImg[row, col] = 255;
else
scaledImg[row, col] = (byte)(255.0 * (value - min) / (max - min));
}
});
}
这只是一个步骤,而其他一些步骤则要复杂得多,但是方法类似。
不幸的是,其中提到的某些东西(例如SIMD / AVX或user3666197的答案)超出了我的能力范围,因此我无法对其进行测试。
将足够的处理负载放入流中以降低帧速率仍然相对容易,但是对于我的应用程序来说,性能现在应该足够了。感谢所有提供输入的人,我将戴的答案标记为已接受,因为我认为它最有帮助。