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

简介:

同样的算法,为什么会有数量级的性能差异?问题起源于几个月前与一位网友的探讨。这位网友在写婚纱抠图程序。一般来说,婚纱摄影的图像都较大,甚至大至几千万像素。这位网友使用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)

 

相关文章
|
缓存 JavaScript 前端开发
IDEA启动VUE前端项目
IDEA启动VUE前端项目操作流程
|
SQL 算法 数据库
OceanBase 查询优化 | 学习笔记
快速学习 OceanBase 查询优化
OceanBase 查询优化 | 学习笔记
|
7月前
|
JSON 监控 API
1688商品列表API接口指南
1688 商品列表 API 可帮助开发者和商家获取商品基本信息(如 ID、名称、价格等)、支持筛选排序(类目、价格、销量等条件)、分页查询及指定店铺商品获取,便于商品管理与竞品分析。调用流程包括:注册账号创建应用以获取 App Key 和 App Secret、生成签名确保请求合法性、构造请求参数(含 app_key、sign 等)、发送 HTTP 请求并处理 JSON 响应数据。
271 19
|
10月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
1140 9
|
11月前
|
Go vr&ar 图形学
重塑体验:AR/VR技术在游戏与娱乐行业的创新应用
【10月更文挑战第29天】本文探讨了AR/VR技术如何改变游戏与娱乐行业,介绍了AR和VR的基本概念及其在游戏和娱乐中的应用实例,包括《精灵宝可梦GO》的AR开发和VR视频播放器的实现代码,并展望了未来的发展趋势。
737 2
|
12月前
|
机器学习/深度学习 网络架构
深度学习之对抗攻击的防御
基于深度学习的对抗攻击防御是一项重要的研究方向,旨在提高模型在面对对抗样本时的鲁棒性和安全性。对抗攻击通常通过向输入数据中添加微小扰动,使得深度学习模型做出错误的预测。
297 1
|
12月前
|
安全 网络安全 数据安全/隐私保护
如何利用AWS CloudFront 自定义设置SSL
如何利用AWS CloudFront 自定义设置SSL
10760 0
|
Dubbo 安全 应用服务中间件
|
Web App开发 移动开发 JavaScript
轻量级 Web 组件编译器 — Stencil
文章中,你将会体验到从头开始构建自己的应用,分别使用Stencil CLI 和 云开发平台
811 2
轻量级 Web 组件编译器 — Stencil