找回密码
 立即注册
首页 业界区 业界 C# 指针之美

C# 指针之美

郦惠 2025-5-29 16:19:32
    将C#图像库的基础部分开源了(https://github.com/xiaotie/GebImage)。这个库比较简单,且离成熟还有一段距离,但它是一种新的开发模式的探索:以指针和非托管内存为主的C#程序开发。
    我许多项目都是在这个库基础上的开发,实战证明了它的有效。从今天起,将断断续续(太忙了)的写一系列文章来介绍这种开发方法,介绍基于此的图像编程。本文便是第一篇。
    以指针和非托管内存为主的C#程序开发,无论对.Net程序员来说,还是对传统的C/C++程序员来说,均属异类。然而这种方法在很多场景下是非常有效的,尤其是图像编程,所谓谈笑间,樯橹灰飞烟灭,不外如是。
    既有C/C++的高性能,又能直接管理内存不给GC带来压力,同时又拥有.net开发的大部分优势,可以快速迭代,何乐而不为呢?
一、简洁优美的代码

    本来初稿这节写了好几百字,将C#指针开发与C/C++开发,Java开发、D语言开发等进行对比,阐述理念。不过现在觉得,阐述一个新事物,没有比用例子更直接的了。
    例子:打开一张图像,先将它转化为灰度图像,再进行二值化(变成黑白图像),然后进行染色,将白色的像素变成红色。以上每一个过程都弹出窗体显示出来。
    代码截图更有视觉冲击力:

1.jpeg
 
    像诗歌一样简洁和优美,这就是孤的代码。具备C/C++的高性能和C#的行云流水,同时又有IDE的强大生产力相助,说这些话已属多余,看见这样的代码,更应该想到的是:妹纸,今天工作全部搞定,现在有空吗,哥来接你。
    这才是工作,这才是生活。留下时间,看看书,看看漫画,玩玩乐乐。最近在看《偷星九月天》,就拿沧殿来测试这段程序吧:
 
2.jpeg

改编    程序员A:那帮孙子又新提了几百条需求,老大,你要带我们突围吗?
    程序员B:不是突围,是杀光它们!
 
     下面,请跟随我,来一段短程探险吧。
     (本文中的代码可在 https://github.com/xiaotie/GebImage/tree/develop 处下载。打包下载地址见 集异璧图像与视觉分析库 )
二、C# 指针基础

    在C#中使用指针,需要在项目属性中选中“Allow unsafe code”:

3.jpeg
      接着,还需要在使用指针的代码的上下文中使用unsafe关键字,表明这是一段unsafe代码。
    可以用unsafe {  } 将代码围住,如:
                     unsafe
                     {
                         new ImageArgb32(path).ShowDialog("原始图像")
                             .ToGrayscaleImage().ShowDialog("灰度图像")
                             .ApplyOtsuThreshold().ShowDialog("二值化图像")
                             .ToImageArgb32()
                             .ForEach((Argb32* p) => { if (p->Red == 255) *p = Argb32.RED; })
                             .ShowDialog("染色");
                     }
    也可在方法或属性上加入unsafe关键字,如:
     private unsafe void btnSubmit_Click(object sender, EventArgs e) 
    也可在class或struct 上加上unsafe 关键字,如:
     public partial unsafe class FrmDemo1 : Form    指针配合fixed关键字可以操作托管堆上的值类型,如:
    public unsafe class Person
    {
        public int Age;

        public void SetAge(int age)
        {
            fixed (int* p = &Age)
            {
                *p = age;
            }
        }
    }    指针可以操作栈上的值类型,如:
             int age = 0;
             int* p = &age;
             *p = 20;
             MessageBox.Show(p->ToString()); 
    指针也可以操作非托管堆上的内存,如:
             IntPtr handle = System.Runtime.InteropServices.Marshal.AllocHGlobal(4);
             Int32* p = (Int32*)handle;
             *p = 20;
             MessageBox.Show(p->ToString());
             System.Runtime.InteropServices.Marshal.FreeHGlobal(handle);
    System.Runtime.InteropServices.Marshal.AllocHGlobal 用来从非托管堆上分配内存。System.Runtime.InteropServices.Marshal.FreeHGlobal(handle)用来释放从非托管对上分配的内存。这样我们就可以避开GC,自己管理内存了。
三、几种常用用法

    1、使用Dispose模式管理非托管内存
    如果使用非托管内存,建议用Dispose模式来管理内存,这样做有以下好处: 可以手动dispose来释放内存;可以使用using 关键字开管理内存;即使不释放,当Dispose对象被GC回收时,也会收回内存。
    下面是Dispose模式的简单例子:
4.gif
5.gif
View Code  1         public unsafe class UnmanagedMemory : IDisposable
 2         {
 3             public int Count { get; private set; }
 4 
 5             private byte* Handle;
 6             private bool _disposed = false;
 7 
 8             public UnmanagedMemory(int bytes)
 9             {
10                 Handle = (byte*) System.Runtime.InteropServices.Marshal.AllocHGlobal(bytes);
11                 Count = bytes;
12             }
13 
14             public void Dispose()
15             {
16                 Dispose(true);
17                 GC.SuppressFinalize(true);
18             }
19 
20             protected virtual void Dispose( bool isDisposing )
21             {
22                 if (_disposed) return;
23                 if (isDisposing)
24                 {
25                     if (Handle != null)
26                     {
27                         System.Runtime.InteropServices.Marshal.FreeHGlobal((IntPtr)Handle);
28                     }
29                 }
30                 _disposed = true;
31             }
32 
33             ~UnmanagedMemory()
34            {
35               Dispose( false );
36            }
37         }    使用:
            using (UnmanagedMemory memory = new UnmanagedMemory(10))
            {
                int* p = (int*)memory.Handle;
                *p = 20;
                MessageBox.Show(p->ToString());
            }
    2、使用 stackalloc 在栈中分配内存
    C# 提供了stackalloc 关键字可以直接在栈中分配内存,一般情况下,使用栈内存会比使用堆内存速度快,且栈内存不用担心内存泄漏。下面是例子:
             int* p = stackalloc int[10];
             for (int i = 0; i Red = 200; });    用ForEach测试,对100万像素的图像设置Red通道值为200,循环100次,我的测试结果是 400 ms,约是直接循环的 4-5 倍。可见这是个性能不高的操作(其实也够高了,100万象素,循环100遍,耗时400ms),可以在对性能要求不是特别高时使用。
八、与C/C++的比较

    我测试了很多场景,C# 下指针性能约是 C/C++ 的 70-80%,性能差距,可以忽略。
    相对于C/C++来说,C#无法直接操作硬件是其遗憾,这种情况,可以使用C/C++写段小程序来弥补,不过,我还没遇到这种场景。很多情况都可以P/Invoke解决。
    做图像的话,很多时候需要使用显卡加速,如使用CUDA或OpenCL,幸运的是,C#也可以直接写CUDA或OpenCL代码,但是功能可能会受到所用的库的限制。也可以用传统方式写CUDA或OpenCL代码,再P/Invoke调用。如果用传统的C/C++开发的话,也需要做同样的工作。

和C比较:
    这套方案比C的抽象程度高,我们有模板,有lambda表达式,还有一大票的语法糖。在类库上,比C的类库完善的多。我们还有反射,有命名空间等等一大票的东西。

和C++比较:
    这套方案的抽象程度比C++要低一些。毕竟,值类型无法继承,模板机制比C++ 差一点。但是在生产力上比C++要高很多。抛开C++那一大票陷阱不说,以秒计算的编译速度就够让C++程序员流口水的。当我们在咖啡馆里约会喝咖啡时,C++程序员还正端着一杯咖啡坐在电脑前等待程序编译结束。
九、接下来的工作

    接下来的工作主要有两个:
    内联工具:C# 的内联还不够强大。需要一个内联工具,对想要内联的方法使用特性标记一下,在编译结束后,在IL代码层面内联。
    翻译工具:移动开发是个痛。如何将C#的代码翻译成C/C++的代码,在缺乏.Net的运行时下运行?

    这两个工作都不紧要。C#内联效果不好的地方(这种情况很少),可以手动内联。至于移动开发嘛,在哥的一云三端大计中,C# 的定位是云图像开发(C#+CUDA),三端中,桌面运用是用C#和Flash开发,Web和移动应用使用Flash开发,没有C#的事情。
    C/C++ 呢?更没有它们的位置啦!不对,还是有的。用它们来开发Flash应用的核心算法!够另类吧!


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册