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

简介:

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

相关文章
|
监控 Java 图形学
【性能优化篇】U3D游戏卡顿大作战:内存与渲染效率的极致提升
【7月更文第12天】在Unity3D游戏开发领域,性能优化是决定玩家体验好坏的关键一环。游戏频繁卡顿,不仅破坏了沉浸式体验,还可能造成玩家流失。本文将深入探讨如何有效解决U3D游戏卡顿问题,特别聚焦于内存管理和渲染效率两大核心领域,助力开发者打造流畅丝滑的游戏世界。
960 0
|
Java Docker 容器
docker-compose部署一个springboot项目(包含环境)
docker-compose部署一个springboot项目(包含环境)
531 0
TabLayout、ViewPager和Fragment之间的通讯
TabLayout、ViewPager和Fragment之间的通讯
|
Java jenkins Linux
Jenkins保姆级使用:Jenkins部署springboot项目,手把手实战经历
Jenkins是一个基于Java开发的开源的实现项目可持续集成的工具,解决了平时开发项目之后需要手动打包与发布项目的问题,将这个流程实现自动化;需要了解更多关于Jenkins的内容可以点击 官网 因为项目需要,近期要准备使用jenkins部署springboot单服务项目,详细记录一下其中遇到的问题和使用过程,留下脚印。
7432 0
|
存储 传感器 Python
[oeasy]python0132_变量含义_meaning_声明_declaration_赋值_assignment
[oeasy]python0132_变量含义_meaning_声明_declaration_赋值_assignment
143 0
[oeasy]python0132_变量含义_meaning_声明_declaration_赋值_assignment
|
Kubernetes 网络协议 NoSQL
调试 Docker 容器内部进程
本文以 Nebula Graph 进程为例,讲解如何不破坏原有容器的内容,也不用在其中安装任何的工具包前提下,像在本地一样来调试进程
3170 0
|
Java API
JDK 每半年就会更新一次新特性,再不掌握就要落伍了:JDK8 的新特性
从 2017 年开始,JDK 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中仍有近半的环境使用 JDK8,有近半的人转移到了 JDK11,随着 JDK17 的发布,相信比例会有所变化。 ———————————————— 版权声明:本文为CSDN博主「看山」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/liuxinghao/article/details/1215
291 0
JDK 每半年就会更新一次新特性,再不掌握就要落伍了:JDK8 的新特性
|
3天前
|
人工智能 运维 安全
|
1天前
|
人工智能 异构计算
敬请锁定《C位面对面》,洞察通用计算如何在AI时代持续赋能企业创新,助力业务发展!
敬请锁定《C位面对面》,洞察通用计算如何在AI时代持续赋能企业创新,助力业务发展!
|
5天前
|
SpringCloudAlibaba 负载均衡 Dubbo
微服务架构下Feign和Dubbo的性能大比拼,到底鹿死谁手?
本文对比分析了SpringCloudAlibaba框架下Feign与Dubbo的服务调用性能及差异。Feign基于HTTP协议,使用简单,适合轻量级微服务架构;Dubbo采用RPC通信,性能更优,支持丰富的服务治理功能。通过实际测试,Dubbo在调用性能、负载均衡和服务发现方面表现更出色。两者各有适用场景,可根据项目需求灵活选择。
398 124
微服务架构下Feign和Dubbo的性能大比拼,到底鹿死谁手?