重新认识C#: 玩转指针

本文探讨了在 C# 中使用指针进行高效开发的方法,包括如何利用 unsafe 关键字和 Lambda 表达式访问非托管内存,以及通过编写自定义泛型辅助类提升图像处理性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    许多文章并不鼓励在C#下使用指针开发,不过,本文偏偏要这样做。在大量尝试C#下使用指针开发之后,你会对C#有更深的认识。

    在说C#下的指针之前,需要提一下C++/CLI。C++/CLI 我们可以把它看作两部分:Native C++和 Managed C++,两者可以无缝结合。对C#,我们也可以把它看作两部分:Managed C# 和 Unmanaged C#。Managed C# 和 Unmanaged C# 是我杜撰的两个词,前者就是我们通常的C#,后者就是使用指针、Struct和非托管内存的C#。事实证明,Unmanaged C#也可以玩的十分优雅——它具有C语言的大部分特性,却比C语言好用的多。 C# 与 C++/CLI之间的对应关系见下图:

image 

    C++/CLI默认是 Native C++,而C# 默认是 Managed C# 。除了不能内嵌汇编以及编译方式不同之外,C++/CLI和C#两者在层面上几乎是等价的。其中,C++/CLI略微偏底层一点,C#略微偏高层一点。尽管略微偏高层一点,C#仍然可以当成准系统语言来玩。你可以将Unmanaged C# 当作 mini c 来玩,区别只是,C 语言一般是编译执行的,而 Unmanaged C# 是先编译成 IL ,再使用Ngen编译成机器码或在运行时编译成机器码执行。

    在C#下不鼓励使用指针,这是因为C#的定位是应用级的开发,如果我们把它定位为低一级别的开发,那么,就需要大量的使用指针了。大量使用指针进行Unmanaged C#开发,“本质”上就是使用 C 语言。只是因为目前 JIT 技术发展年代仍不够久远,导致 Unmamaged C# 的性能较 C 语言 略低。

    下面,画张图,描述一下当下的C#语言。

 

 

 

    当下的C#包含了五种编程范式:类C、OO、泛型、Lambda、Dynamic。关于 OO、泛型、Lambda、Dynamic已经有很多文章介绍和总结了,关于类C这一块却很少有人写文章详细介绍。就像Ajax重新发现了javascript一样,我们也应该去重新发现C#中的Unmanaged 成分

    回看程序设计语言的发展史。C语言是一直的王者。但是由于抽象能力不足,在C的基础上出现了C++,后来又出现了帮你管家的保姆Java,于是,在系统层开发使用C/C++,在应用层开发使用Java成为一种常见的分工方式。有没有一种语言同时具备Java的快速开发优势和C/C++的高性能且能直接访问内存这两个优点呢?D语言就是奔着这个目标设计的。许多人对D语言报以厚望。可问题是,D语言看起来很美,但太草根了,各方面都不成熟。C#诞生之后,人们认为它和Java的定位是类似的,我也一直这样认为。同时,我还在寻找能够快速开发、自己管理内存、拥有庞大的类库的另一种语言,来进行高性能开发及实时开发。我看过D语言,看过Haskell语言,都不是我想要的,转一圈回来,发现,原来答案已在自己的手中,那就是已经用了很多年的C#——C#的Unmanaged部分。

    开发过一个软实时系统,每秒钟有数百万对象生灭,是使用C++开发的。C++开发效率低下,我想寻找一个替代品。最先找到的是Java,由于GC的存在,在Java下开发软实时系统比较困难,以至于出现了专门的Java实时规范和实时Java虚拟机。当时接触C#不久,想,为什么C#下没人研究实时系统?现在知道了答案:那就是开发实时性应用,相对于Java,C#具有非常大的优势——由于Unmanaged 部分的存在,不需要专门的C#实时虚拟机。C# 中,GC 是无法直接插手非托管内存的,如果只有寥寥无几的对象在托管内存中,每一次GC时间十分短暂,可以忽略不计。

    这两年开始进行图像处理方面的程序开发。图像处理开发,C/C++是王道。不过,C/C++开发效率低下是个大问题,同样需要寻找替代品。最开始我使用的是C++/CLI,使用后发现,C++/CLI 不好用,它继承了C++的所有缺点,最不能忍受的是狂慢的编译速度。C++/CLI的CLI部分虽然可以使用.Net的庞大的类库,但是没有C#自然。有没有一种更好的方式平衡开发效率和运行效率?有!那就是打开unsafe之后的C#:优雅的语法、快速的编译、庞大的类库、完美的IDE、想托管内存就托管内存,不想托管就不托管——犀利!非常的犀利!无比的犀利!。

    在《编写高效的C#图像处理程序——我的实验》和《编写高效的C#图像处理程序——我的实验(续)》两篇文章中,我使用指针,得到了近似C语言的性能。因此,不必担心C#的性能。

    C#目前包括的五种编程范式:类C、OO、泛型、Lambda、Dynamic,这五种编程范式几乎可以无缝的结合,熟练使用这些编程范式,可以把C#下的指针玩的天花乱坠:

    (1)Class和Struct中可以直接包含指针成员,这样,我们可以设计一套自己的继承体系(当然,得在托管内存中。不过,可以将性能攸关部分放在非托管内存中,然后,将它的指针放到Class中,遵循Disposable模式来管理,避免内存泄漏。)

    (2)C#下的泛型不支持泛型Class的指针,于是,我在《C#模板编程(1):有了泛型,为什么还需要模板?》和《C#模板编程(2): 编写C#预处理器,让模板来的再自然一点》这两篇文章中编写了C#的预处理器,再结合using关键字和partial关键字实现了对C++模板的模拟,用以Unmanaged C#代码的强类型复用。

    这样处理,就写出了几个纯C#开发的高性能C#图像处理基本类,见博文《发布我的高性能纯C#图像处理基本类,顺便也挑战一下极限。:)》。

    这些基本类可以通过指针访问图像的像素,也可以通过索引器来访问像素,也可以通过迭代器来访问像素。通过指针访问速度最快,但比较麻烦。通过索引器和迭代器访问比较慢,但比较方便。不过,通过索引器和迭代器来访问像素很容易误用,比如说,假设图像是A。A[1,2]可以获得图像的第1行(首行为第0行),第2列(首列为第0列)的像素。假设想更改这个像素的Red值为5,这样写是无效的:A[1,2].Red = 5。因为,A[1,2]是一个Struct实例,它是坐标为(1,2)的像素值的“快照”,对A[1,2]的修改无法写入到图像像素中去,需要这样写才能实现真正的修改:Rgb24 item = A[1,2];item.Red = 5; A[1,2]=item。同理,通过迭代器访问,也无法修改像素具体值。

    这样处理既不优雅,又容易误用。怎么办呢?思来想去,我决定取消它!改用另一种方式提供对图像像素的便捷访问。什么办法呢?Lambda表达式!可是,问题来了,C#下的泛型不支持具体的指针类型作为泛型类型,好在关上了一扇门,C#又打开了另一扇门——delegate 支持指针类型!于是,使用《C#模板编程(1):有了泛型,为什么还需要模板?》和《C#模板编程(2): 编写C#预处理器,让模板来的再自然一点》这两篇文章中提出的C#模板开发技巧,编写代码,有:

ExpandedBlockStart.gif ImageClassHelper_Template.cs
 1  using  TPixel  =  System.Byte; 
 2  using  TCache  =  System.Int32; 
 3  using  TKernel  =  System.Int32; 
 4 
 5  using  System; 
 6  using  System.Collections.Generic; 
 7  using  System.Text; 
 8 
 9  namespace  Orc.SmartImage.Hidden 
10 
11       static   class  ImageClassHelper_Template 
12      { 
13           #region  mixin 
14 
15           public   unsafe   delegate   void  ActionOnPixel(TPixel *  p); 
16           public   unsafe   delegate  Boolean PredicateOnPixel(TPixel *  p); 
17 
18           public   unsafe   static   void  ForEach( this  UnmanagedImage < TPixel >  src, ActionOnPixel handler) 
19          { 
20              TPixel *  start  =  (TPixel * )src.StartIntPtr; 
21              TPixel *  end  =  start  +  src.Length; 
22               while  (start  !=  end) 
23              { 
24                  handler(start); 
25                   ++ start; 
26              } 
27          } 
28 
29           public   unsafe   static  Int32 Count( this  UnmanagedImage < TPixel >  src, PredicateOnPixel handler) 
30          { 
31              TPixel *  start  =  (TPixel * )src.StartIntPtr; 
32              TPixel *  end  =  start  +  src.Length; 
33              Int32 count  =   0
34               while  (start  !=  end) 
35              { 
36                   if  (handler(start)  ==   true ) count ++
37                   ++ start; 
38              } 
39               return  count; 
40          } 
41 
42           public   unsafe   static  Int32 Count( this  UnmanagedImage < TPixel >  src, Predicate < TPixel >  handler) 
43          { 
44              TPixel *  start  =  (TPixel * )src.StartIntPtr; 
45              TPixel *  end  =  start  +  src.Length; 
46              Int32 count  =   0
47               while  (start  !=  end) 
48              { 
49                   if  (handler( * start)  ==   true ) count ++
50                   ++ start; 
51              } 
52               return  count; 
53          } 
54 
55           public   unsafe   static  List < TPixel >  Where( this  UnmanagedImage < TPixel >  src, PredicateOnPixel handler) 
56          { 
57              List < TPixel >  list  =   new  List < TPixel > (); 
58 
59              TPixel *  start  =  (TPixel * )src.StartIntPtr; 
60              TPixel *  end  =  start  +  src.Length; 
61               while  (start  !=  end) 
62              { 
63                   if  (handler(start)  ==   true ) list.Add( * start); 
64                   ++ start; 
65              } 
66 
67               return  list; 
68          } 
69 
70           public   unsafe   static  List < TPixel >  Where( this  UnmanagedImage < TPixel >  src, Predicate < TPixel >  handler) 
71          { 
72              List < TPixel >  list  =   new  List < TPixel > (); 
73 
74              TPixel *  start  =  (TPixel * )src.StartIntPtr; 
75              TPixel *  end  =  start  +  src.Length; 
76               while  (start  !=  end) 
77              { 
78                   if  (handler( * start)  ==   true ) list.Add( * start); 
79                   ++ start; 
80              } 
81 
82               return  list; 
83          } 
84 
85           #endregion  
86      } 
87 

 

    这样一来,就提供了ForEach扩展方法,可以通过指针直接访问具体的像素。同时,我也顺便实现了Count和Where两个扩展方法。Count和Where两个扩展方法同时提供了指针版本和非指针版本。

    然后,编写类 Rgb24ImageClassHelper:

ExpandedBlockStart.gif Rgb24ImageClassHelper.cs
 1  using  System; 
 2  using  System.Collections.Generic; 
 3  using  System.Text; 
 4 
 5  namespace  Orc.SmartImage 
 6 
 7       using  TPixel  =  Rgb24; 
 8       using  TCache  =  System.Int32; 
 9       using  TKernel  =  System.Int32; 
10 
11       public   static   partial   class  Rgb24ImageClassHelper 
12      { 
13           #region  include "ImageClassHelper_Template.cs" 
14           #endregion  
15      } 
16  }

 

    编译之后,就可以通过Lambda表达式通过指针来访问 UnmanagedImage<Rgb24> 实例中的像素。例子&性能测试为:

ExpandedBlockStart.gif 例子与性能测试代码
 1  Rgb24Image rgb24  =   new  Rgb24Image(map); 
 2 
 3  //  将每个像素的Blue值改为 50
 4 
 5  CodeTimer.Time( " ForEachByLambdaWithPointer- "   +  imgName,  1 , ()  =>  
 6 
 7      rgb24.ForEach((Rgb24 *  p)  =>  { p -> Blue  =   50 ; }); 
 8      Console.WriteLine(rgb24.Start -> Blue); 
 9  }); 
10 
11  CodeTimer.Time( " ForEachByPointer- "   +  imgName,  1 , ()  =>  
12 
13      Rgb24 *  start  =  rgb24.Start; 
14      Rgb24 *  end  =  rgb24.Start  +  rgb24.Length; 
15       while  (start  !=  end) 
16      { 
17          start -> Blue  =   50
18           ++ start; 
19      } 
20      Console.WriteLine(rgb24.Start -> Blue); 
21  }); 
22 
23  CodeTimer.Time( " CountByLambdaWithPointer- "   +  imgName,  1 , ()  =>  
24 
25      Console.WriteLine(rgb24.Count((Rgb24 *  p)  =>  {  return  p -> Blue  >   50 ; })); 
26  }); 
27 
28  CodeTimer.Time( " CountByLambdaWithValue- "   +  imgName,  1 , ()  =>  
29 
30      Console.WriteLine(rgb24.Count((Rgb24 c)  =>  {  return  c.Blue  >   50 ; })); 
31  }); 
32 
33  CodeTimer.Time( " WhereByLambdaWithPointer- "   +  imgName,  1 , ()  =>  
34 
35      Console.WriteLine(rgb24.Where((Rgb24 *  p)  =>  {  return  p -> Blue  >   50 ; }).Count); 
36  }); 
37 
38  CodeTimer.Time( " WhereByLambdaWithValue- "   +  imgName,  1 , ()  =>  
39 
40      Console.WriteLine(rgb24.Where((Rgb24 c)  =>  {  return  c.Blue  >   50 ; }).Count); 
41  });

 

    测试结果:

ExpandedBlockStart.gif 测试结果
 1  ForEachByLambdaWithPointer - 5000_6000_24 
 2  50  
 3          Time Elapsed:   210ms 
 4          CPU Cycles:      333 , 752 , 386  
 5          Gen  0 :           0  
 6          Gen  1 :           0  
 7          Gen  2 :           0  
 8 
 9  ForEachByPointer - 5000_6000_24 
10  50  
11          Time Elapsed:   76ms 
12          CPU Cycles:      116 , 868 , 697  
13          Gen  0 :           0  
14          Gen  1 :           0  
15          Gen  2 :           0  
16 
17  CountByLambdaWithPointer - 5000_6000_24 
18  0  
19          Time Elapsed:   249ms 
20          CPU Cycles:      425 , 180 , 283  
21          Gen  0 :           0  
22          Gen  1 :           0  
23          Gen  2 :           0  
24 
25  CountByLambdaWithValue - 5000_6000_24 
26  0  
27          Time Elapsed:   484ms 
28          CPU Cycles:      850 , 295 , 952  
29          Gen  0 :           0  
30          Gen  1 :           0  
31          Gen  2 :           0  
32 
33  WhereByLambdaWithPointer - 5000_6000_24 
34  0  
35          Time Elapsed:   242ms 
36          CPU Cycles:      425 , 229 , 156  
37          Gen  0 :           0  
38          Gen  1 :           0  
39          Gen  2 :           0  
40 
41  WhereByLambdaWithValue - 5000_6000_24 
42  0  
43          Time Elapsed:   496ms 
44          CPU Cycles:      855 , 667 , 758  
45          Gen  0 :           0  
46          Gen  1 :           0  
47          Gen  2 :           0

 

    可见:使用Lambda表达式通过指针来访问像素比使用指针直接访问像素慢,大概速度是后者的 1/2 - 1/3 。而使用Lambda表达式通过值来访问像素比使用Lambda表达式通过指针来访问像素要慢。大概速度是后者的1/2。虽然速度慢下来了,但对于性能不攸关的地方,这样处理还是值得的——使用Lambda表达式可以让代码更简洁更优雅!

    好了,现在,类C,OO,泛型/模板,Lambda表达式就全揉在一起了,至于具体怎么用,就看具体情况下的权衡取舍了。如果再玩玩Dynamic,大概会有更有趣的玩法。

    现在看来,C#真是太NB了!通吃啊!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值