重新認識C#: 玩轉指針

作者: xiaotie  來源: 博客園  發布時間: 2010-04-10 21:53  閱讀: 2354 次  推薦: 0   原文鏈接   [收藏]  

  許多文章并不鼓勵在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# 默認是 Unmanaged 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#模板開發技巧,編寫代碼,有:

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:

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 實例中的像素。例子&性能測試為:

例子與性能測試代碼
 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 });

  測試結果:

測試結果
 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了!通吃啊!

0
1
 
標簽:CSharp
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()