也谈谈性能:局部性与性能的实验观察

简介:

同样的算法,为什么会有数量级的性能差异?问题起源于几个月前与一位网友的探讨。这位网友在写婚纱抠图程序。一般来说,婚纱摄影的图像都较大,甚至大至几千万像素。这位网友使用C#开发,他的问题就在于性能。当时建议他使用XNA开发,但问题又出来了:加载一副图像的时间竟需要好几秒!而我自己写的C#加载几千万像素图像及图像转换操作,都是瞬间完成。为什么会有如此大的差别呢?这就是本文要说的。

问题主要处在程序的局部性和缓存命中上。我们把图像类抽象一下:

Bitmap 

    Width,Height; 
    Data; 
}

一般来说,它在内存中被分为两块存放:

image

程序会分配一大块内存,存储具体的图像数据,然后再为Bitmap分配小块内存,储存Width、Height及对图像数据的引用。

这样一来:对于图像的操作的两种写法,性能上就会存在差异:

写法A:

for(int y = 0; y<xxx.Height; y++) 

  for(int x = 0; x < xxx.Width; x++) 
  { 
     Data[x,y] = .... 
  } 
}

写法B:

int width = xxx.Width; 
int height = xxx.Height; 
for(int y = 0; y<height ; y++) 

  for(int x = 0; x <width ; x++) 
  { 
     Data[x,y] = .... 
  } 
}

写法B中,使用的是在栈中的变量width和height,因此较写法A具有更好的局部性。下面,用实验测试下这两种写法究竟有多大的性能差异。

====

实验1:对托管内存中3000万像素图像的赋值操作

public class Image 

    public int Width { get; set; } 
    public int Height { get; set; } 
}

public class Bitmap : Image 

    public int[] Data;

    public Bitmap(int width, int height) 
    { 
        this.Width = width; 
        this.Height = height; 
        Data = new int[width*height]; 
    }

    public void Fill(int value) 
    { 
        int height = Height; 
        int width = Width;

        for (int y = 0; y < height; y++) 
        { 
            for (int x = 0; x < width; x++) 
            { 
                Data[y * width + x] = value; 
            } 
        } 
    }

    public void FillEx(int value) 
    { 
        for (int y = 0; y < Height; y++) 
        { 
            for (int x = 0; x < Width; x++) 
            { 
                Data[y * Width + x] = value; 
            } 
        } 
    }

    public static void Test() 
    { 
        Bitmap img = new Bitmap(5000, 6000); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
    } 
}

结果见下表(单位ms)。

1 2 3 4
Fill 126(82) 83 84 85
FillEx 100(141) 99 100 99

“1”列结果中括号数据是将Fill和FillEx执行顺序互换后的测试结果。可以发现,同一个类中,首次执行的方法吃点亏。对于这个测试来说,吃 40 ms 的亏。比较2,3,4可以看出,FillEx要比Fill慢一点。

====

实验2:对非托管内存中3000万像素图像的赋值操作

public class UnmanagedBitmap : Image 

    public IntPtr Data;

    public UnmanagedBitmap(int width, int height) 
    { 
        this.Width = width; 
        this.Height = height; 
        Data = Marshal.AllocHGlobal(sizeof(int) * width * height); 
    }

    public unsafe void Fill(int value) 
    { 
        int height = Height; 
        int width = Width; 
        int* p = (int*)Data;

        for (int y = 0; y < height; y++) 
        { 
            for (int x = 0; x < width; x++) 
            { 
                *p = value; 
                p++; 
            } 
        } 
    }

    public unsafe void FillEx(int value) 
    { 
        int* p = (int*)Data; 
        for (int y = 0; y < Height; y++) 
        { 
            for (int x = 0; x < Width; x++) 
            { 
                *p = value; 
                p++; 
            } 
        } 
    }

    public static void Test() 
    { 
        UnmanagedBitmap img = new UnmanagedBitmap(5000, 6000); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
        CodeTimer.Time("Fill", 1, () => { img.Fill(1); }); 
        CodeTimer.Time("FillEx", 1, () => { img.FillEx(2); }); 
    } 
}

测试结果:

1 2 3 4
Fill 128(83) 93 84 84
FillEx 88(123) 90 84 84

可看出,Fill和FillEx几乎没有差别。

====

实验1中,FillEx的循环为:

        for (int y = 0; y < Height; y++) 
        { 
            for (int x = 0; x < Width; x++) 
            { 
                Data[y * Width + x] = value; 
            } 
        }

实验2中,FillEx的循环为:

    public unsafe void FillEx(int value) 
    { 
        int* p = (int*)Data; 
        for (int y = 0; y < Height; y++) 
        { 
            for (int x = 0; x < Width; x++) 
            { 
                *p = value; 
                p++; 
            } 
        } 
    }

对比这两段代码,参照实验结果可以看出,在for循环条件中的Width和Height属性,jit做了特殊的处理,应该是缓存起来了,因此,实验2中的Fill和FillEx才没出现性能差别。而在循环体中所使用的Width属性,没有缓存起来,导致实验1中,Fill和FillEx性能有可见的差异。

尽管如此,实验1所显示的两种写法的性能差别并不大。下面,请看实验3。

====

实验3:两种写法产生数量级性能差异的实验

本实验中所使用的核心类见《发布我的高性能纯C#图像处理基本类,顺便也挑战一下极限。:)》。具体代码可在http://smartimage.googlecode.com/svn/trunk/ 下载。

测试的两个方法如下,两个方法的不同之处我用红色显著标示出来:

public unsafe void ToBitmap(Bitmap map) 

    if (map == null) throw new ArgumentNullException("map"); 
    if (map.Width != this.Width || map.Height != this.Height) 
    { 
        throw new ArgumentException("尺寸不匹配."); 
    }

    if (map.PixelFormat != PixelFormat.Format32bppArgb) 
    { 
        throw new ArgumentException("只支持 Format32bppArgb 格式。 "); 
    }

    Int32 step = SizeOfT(); 
    Byte* t = (Byte*)StartIntPtr;

    BitmapData data = map.LockBits(new Rectangle(0, 0, map.Width, map.Height), ImageLockMode.ReadWrite, map.PixelFormat); 
    try 
    { 
        
int width = map.Width; 
        int height = map.Height;

        Byte* line = (Byte*)data.Scan0;

        for (int h = 0; h < height; h++) 
        { 
            Argb32* c = (Argb32*)line; 
            for (int w = 0; w < width; w++) 
            { 
                m_converter.Copy(t, c); 
                t += step; 
                c++; 
            } 
            line += data.Stride; 
        } 
    } 
    finally 
    { 
        map.UnlockBits(data); 
    } 
}

public unsafe void ToBitmapEx(Bitmap map) 

    if (map == null) throw new ArgumentNullException("map"); 
    if (map.Width != this.Width || map.Height != this.Height) 
    { 
        throw new ArgumentException("尺寸不匹配."); 
    }

    if (map.PixelFormat != PixelFormat.Format32bppArgb) 
    { 
        throw new ArgumentException("只支持 Format32bppArgb 格式。 "); 
    }

    Int32 step = SizeOfT(); 
    Byte* t = (Byte*)StartIntPtr;

    BitmapData data = map.LockBits(new Rectangle(0, 0, map.Width, map.Height), ImageLockMode.ReadWrite, map.PixelFormat); 
    try 
    { 
        Byte* line = (Byte*)data.Scan0;

        for (int h = 0; h < map.Height; h++) 
        { 
            Argb32* c = (Argb32*)line; 
            for (int w = 0; w < map.Width; w++) 
            { 
                m_converter.Copy(t, c); 
                t += step; 
                c++; 
            } 
            line += data.Stride; 
        } 
    } 
    finally 
    { 
        map.UnlockBits(data); 
    } 
}

测试代码:

public static void Test() 

    ImageArgb32 src = new ImageArgb32(5000, 6000); 
    System.Drawing.Bitmap dst = new System.Drawing.Bitmap(5000, 6000, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 
    CodeTimer.Time("ToBitmap",1,()=>{ src.ToBitmap(dst);}); 
    CodeTimer.Time("ToBitmapEx", 1, () => { src.ToBitmapEx(dst); }); 
    CodeTimer.Time("ToBitmap", 1, () => { src.ToBitmap(dst); }); 
    CodeTimer.Time("ToBitmapEx", 1, () => { src.ToBitmapEx(dst); }); 
    CodeTimer.Time("ToBitmap", 1, () => { src.ToBitmap(dst); }); 
    CodeTimer.Time("ToBitmapEx", 1, () => { src.ToBitmapEx(dst); }); 
    CodeTimer.Time("ToBitmap", 1, () => { src.ToBitmap(dst); }); 
    CodeTimer.Time("ToBitmapEx", 1, () => { src.ToBitmapEx(dst); }); 
}

测试结果:

1 2 3 4
ToBitmap 354 259 261 260
ToBitmapEx 7451 7441 7440 7445

由于区别实在太显著,我就没有交换执行次序重复实验了。从结果看出,ToBitmap的写法比ToBitmapEx要快近30倍。到这里可以知道,为什么我在前面提到的那个哥们写的程序加载图像耗时数秒?

下面,我们对ToBitmapEx做一个小小的变动,变动内容标红:

public unsafe void ToBitmapEx(Bitmap map) 

    if (map == null) throw new ArgumentNullException("map"); 
    if (map.Width != this.Width || map.Height != this.Height) 
    { 
        throw new ArgumentException("尺寸不匹配."); 
    }

    if (map.PixelFormat != PixelFormat.Format32bppArgb) 
    { 
        throw new ArgumentException("只支持 Format32bppArgb 格式。 "); 
    }

    Int32 step = SizeOfT(); 
    Byte* t = (Byte*)StartIntPtr;

    BitmapData data = map.LockBits(new Rectangle(0, 0, map.Width, map.Height), ImageLockMode.ReadWrite, map.PixelFormat); 
    try 
    { 
        Byte* line = (Byte*)data.Scan0; 
        
int width = map.Width; 
        for (int h = 0; h < map.Height; h++) 
        { 
            Argb32* c = (Argb32*)line; 
            for (int w = 0; w < width; w++) 
            { 
                m_converter.Copy(t, c); 
                t += step; 
                c++; 
            } 
            line += data.Stride; 
        } 
    } 
    finally 
    { 
        map.UnlockBits(data); 
    } 

测试结果:

1 2 3 4
ToBitmap 313(263) 261 261 260
ToBitmapEx 268(313) 261 264 261

可以看出两者结果已经几乎一样了。

====

总结:

(1)部分情况(实验2),jit可以对程序的局部性做完全优化。

(2)部分情况(实验1),jit可以对程序的局部性做部分优化。

(3)部分情况(实验3),jit对程序的局部性不做优化。

编译优化的原则比较保守,它首先需要保证正确性。比如下面这段代码:

for(int i=0; i< xxx.Width; i++)

{

    xxx.Width = 3;

}

jit就不能简单优化成:

int w = xxx.Width;

for(int i=0; i< w; i++)

{

    xxx.Width = 3;

}

而实际情况可能比这种情况更复杂,jit优化会非常谨慎,很难达到最优效果。在写高性能程序时,不应该依托于jit的优化。对于实验3这种情况,直接把需要使用的分散在内存中各处的数据缓存到栈中即可。

====

多说几句。C#程序中出现的性能问题一般来说和语言与底层机制关系不大。UI性能低下的根源应该在于过度封装,如果有第三方轻量级UI库,那性能肯定是棒棒的。其它方面的性能问题主要还是和设计有关。C#程序和C/C++的性能差异最主要的区别是关注点不一样,C/C++的公用的库设计的一个很重要的目标就是性能,而C#目前的主要的库在设计时偏好是其它方面,C#程序员在写程序时偏好也是其它方面。一个良好设计的C#程序的性能应该不低于C/C++程序的50%。对于复杂程序,由于C/C++的设计复杂度较高,在同等时间内,C#程序的设计应该优秀于C/C++程序,因此在性能上应该达到C/C++的70%才对。

《大唐双龙传》中,寇仲、徐子陵的井中月至境,师仙子的剑心通明,石之轩的入微,伏难陀的梵我不二(tmd黄易这个老小子竟然没给我家可爱的婠婠的天魔功十八层取一个类似井中月至境、剑心通明、入微、梵我不二这样响亮的名字!),都是相通的。

本文转自xiaotie博客园博客,原文链接http://www.cnblogs.com/xiaotie/archive/2010/07/02/1769616.html如需转载请自行联系原作者


xiaotie 集异璧实验室(GEBLAB)

 

相关文章
|
测试技术 5G iOS开发
【游戏专项测试】弱网测试-测试用例
【游戏专项测试】弱网测试-测试用例
|
5月前
|
JSON 前端开发 数据可视化
大模型应用:本地大模型API调用鉴权可视化全解析:从基础实现到业务扩展.45
本文详解本地大模型(如Qwen1.5-1.8B)的轻量级落地方案:基于FastAPI封装带API Key/JWT双鉴权的文本生成API,结合Streamlit构建可视化前端,支持参数调节、IP限流、历史记录与令牌自动刷新,CPU即可运行,兼顾安全性与易用性。
670 5
|
5月前
|
自然语言处理 数据挖掘 BI
Python 生成 PowerPoint 演示文件图表教程
本文教你用Python(python-pptx库)自动化生成PPT图表:从安装配置、创建柱状图/折线图/饼图/散点图,到读取Excel数据、批量制图、美化样式与排版。告别重复手工操作,提升运营、财务、咨询等岗位的报表效率。(239字)
900 2
|
6月前
|
人工智能 运维 关系型数据库
Moltbot实战:MoltBot+RDS AI助手Skill管理RDS实例
本文介绍如何5分钟快速对接Moltbot与阿里云RDS AI助手,打造专属AI数据库运维管家。通过开源Skill实现自动化诊断、参数调优、索引优化等能力,解放DBA于凌晨救火,让重复运维交给AI,专注高价值架构设计。(239字)
Moltbot实战:MoltBot+RDS AI助手Skill管理RDS实例
|
7月前
|
域名解析 缓存 网络协议
蓝易云:海外网站cdn加速服务怎么开启
总结来说,开启海外网站CDN加速服务是一个包括选择CDN服务提供商、购买服务、配置CDN、更改DNS以及测试与优化等阶段的过程。通过细心选择和配置CDN服务,可以大大提升海外用户访问网站的速度,从而提高用户满意度和网站整体性能。
691 7
|
Ubuntu 数据可视化 开发工具
【VTK】ubuntu手动编译VTK9.3 Generating qmltypes file 失败
通过以上步骤,您可以成功解决在Ubuntu上编译VTK 9.3时遇到的 `Generating qmltypes file`失败的问题。关键在于确保系统正确安装了所需的Qt库,并通过CMake配置正确的路径。编译完成后,您将拥有一个功能完备的VTK库,可以用于各种可视化任务。
508 14
大尺度信道建模 | 带你读《大规模天线波束赋形技术原理与设计 》之二十四
本节将介绍 3D 信道模型中 不同传输场景的大尺度衰落模型,包括路损计算、穿透损耗、直射径概率、阴 影衰落等。
大尺度信道建模  | 带你读《大规模天线波束赋形技术原理与设计 》之二十四
|
JavaScript 安全 API
深入理解Vue 3中的Composition API
深入理解Vue 3中的Composition API
668 3
|
机器学习/深度学习 算法 数据挖掘
《KNN与K-means从入门到实战》
《KNN与K-means从入门到实战》
《KNN与K-means从入门到实战》
|
Java
org.springframework.web.util.NestedServletException: Request processing failed; nested exception....
org.springframework.web.util.NestedServletException: Request processing failed; nested exception....
953 0

热门文章

最新文章