C# .NET SIMD底层图像处理性能优化及技术路线咨询
24位BGR转8位灰度图的性能优化问题
测试背景与困惑
我针对3840×2160的Format24bppRgb格式图像(约800万像素)做了性能测试:
- 空循环1036800次耗时3-5ms;
- 仅执行
Avx.LoadVector256操作耗时4-8ms; - 添加
Vector256.Shuffle后耗时升至12ms左右; - 再加入
Avx2.UnpackLow、Avx2.UnpackHigh等操作后耗时达20ms(仅完成24位转8位灰度图); - 未启用
Parallel.For时耗时约90ms。
而OpenCVSharp完成相同格式转换仅需3-6ms,我的空循环耗时居然和OpenCVSharp完整转换耗时差不多,这让我很头疼。我想知道有没有优化方法能把性能提升到3-6ms,或者我的实现思路是不是完全错了?同时希望得到底层图像处理的相关指导,了解当前C# .NET实现还有没有优化空间,或者转向其他语言会不会更合适。
测试代码
const ushort mulBlue = (ushort)(0.114 * 0x10000); const ushort mulGreen = (ushort)(0.587 * 0x10000); const ushort mulRed = (ushort)(0.299 * 0x10000); public static void BGR24Bit2Gray(byte* src, byte* dst, int count) { var CoeB = Vector128.Create(mulBlue); var CoeG = Vector128.Create(mulGreen); var CoeR = Vector128.Create(mulRed); byte[] Seqdata1 = new byte[32] { 0, 3, 6, 9, 12, 15, 18, 21, 1, 4, 7, 10, 13, 16, 19, 22, 2, 5, 8, 11, 14, 17, 20, 23, 0, 0, 0, 0, 0, 0, 0, 0 }; byte[] Seqdata2 = new byte[32] { 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 2, 5, 8, 11, 14, 17, 20, 23, 0, 0, 0, 0, 0, 0, 0, 0 }; int allPixels = count * 3; byte* srcEnd = src + allPixels; int stride = 24; int loopCount = (int)((srcEnd - src) / stride); ParallelOptions pOptions = new ParallelOptions(); pOptions.MaxDegreeOfParallelism = Environment.ProcessorCount; fixed (byte* pa1 = Seqdata1) fixed (byte* pa2 = Seqdata2) { byte* par1 = pa1; byte* par2 = pa2; byte* tdst = dst; Vector256<byte> seq1 = Avx.LoadVector256(par1); Vector128<byte> seq2 = Sse2.LoadVector128(par2); Stopwatch sw = Stopwatch.StartNew(); //for (int i = 0; i < 1036800; i++) //for (int i = 0; i < loopCount; i++) Parallel.For(0, loopCount - 1, pOptions, i => { var _1st24bytes = Avx.LoadVector256(src + i * stride); var rslt256 = Vector256.Shuffle(_1st24bytes, seq1); Vector256<ushort> v1l = Avx2.UnpackLow(rslt256, Vector256<byte>.Zero).AsUInt16(); Vector256<ushort> v1h = Avx2.UnpackHigh(rslt256, Vector256<byte>.Zero).AsUInt16(); var Bdata = v1l.GetLower(); var Gdata = v1h.GetLower(); var Rdata = v1l.GetUpper(); var B = Sse2.MultiplyHigh(Bdata, CoeB); var G = Sse2.MultiplyHigh(Gdata, CoeG); var R = Sse2.MultiplyHigh(Rdata, CoeR); var Gray = Sse2.Add(B, Sse2.Add(G, R)); var GrayByt = Vector128.AsByte(Gray); var rslt128 = Ssse3.Shuffle(GrayByt, seq2); var frslt = rslt128.GetLower(); Sse2.Store(dst + i * 8, rslt128); } ); sw.Stop(); var tp = sw.ElapsedMilliseconds; tslblStatus.Text = tp.ToString() + "ms"; } } //Test Method: private unsafe void ConvertionTest() { Bitmap orgpic = pcbxOrg.Image as Bitmap; //Do nothing when the original image is null if(orgpic==null) return; //Only for 24 bit image processing if(orgpic.PixelFormat != PixelFormat.Format24bppRgb) return; int width = orgpic.Width; int height = orgpic.Height; int count = width * height; //Pixel count //Define the gray image Bitmap despic = new Bitmap(width, height, PixelFormat.Format8bppIndexed); lock (lcobj) { //Lock the image data BitmapData orgData = orgpic.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, orgpic.PixelFormat); BitmapData desData = despic.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed); //Get the image pointer byte* orgPtr = (byte*)orgData.Scan0.ToPointer(); byte* desPtr = (byte*)desData.Scan0.ToPointer(); //Start convertion Stopwatch sw = Stopwatch.StartNew(); BGR24Bit2Gray(orgPtr, desPtr, count); sw.Stop(); //Unlock the image data orgpic.UnlockBits(orgData); despic.UnlockBits(desData); var tp = sw.ElapsedMilliseconds; tslblStatus.Text = tp.ToString() + "ms"; } //ColorPalette for the 8 bit image ColorPalette palette = despic.Palette; for (int i = 0; i != palette.Entries.Length; i++) { palette.Entries[i] = Color.FromArgb(i, i, i); } despic.Palette = palette; pcbxDes.Image = despic; }
优化方向建议
- 重构向量化逻辑:当前Shuffle和Unpack操作过于繁琐,可直接用Avx2的
Permute或Shuffle提取B/G/R通道到独立向量,减少数据拆分合并开销;尽量用256位向量完成计算,避免频繁切换128位向量。 - 优化并行策略:按行拆分任务(而非8像素块),减少线程调度开销;确保每个任务处理连续内存块,提升缓存命中率。
- 内存对齐优化:使用
Avx.LoadAlignedVector256加载对齐内存,LockBits时指定对齐方式或手动调整指针,非对齐访问会大幅降低性能。 - 简化计算指令:用8位整数系数(29、150、76,总和255)替代定点乘法,利用Avx2的
MultiplyAddAdjacent指令一步完成加权求和,避免MultiplyHigh的额外开销。 - 常量复用:将CoeB/CoeG/CoeR、seq1/seq2等常量设为静态只读变量,避免每次调用方法重复创建。
- 底层实现对比:OpenCVSharp依赖C优化代码或IPP硬件加速,若.NET优化到极限仍不达标,可考虑用C/CLI编写核心函数,或使用.NET Native AOT编译提升性能。
内容的提问来源于stack exchange,提问作者Mweidy
相关产品推荐
相关产品推荐

