何时在 C#/.NET 中使用指针?

Posted

技术标签:

【中文标题】何时在 C#/.NET 中使用指针?【英文标题】:When to use pointers in C#/.NET? 【发布时间】:2011-07-07 12:08:11 【问题描述】:

我知道 C# 让程序员能够在不安全的上下文中访问和使用指针。但是什么时候需要呢?

在什么情况下,使用指针成为必然?

仅仅是出于性能原因吗?

另外,为什么 C# 会通过不安全的上下文公开此功能,并从中删除所有托管优势?理论上是否可以在不失去托管环境优势的情况下使用指针?

【问题讨论】:

感谢理查德,只是想通过提出(更多)问题来了解更多信息:O 您可能会对这个问题感兴趣:***.com/questions/584134/… 另见programmers.stackexchange.com/questions/24583/… 【参考方案1】:

什么时候需要?什么情况下使用指针成为必然?

当托管安全解决方案的净成本不可接受但不安全解决方案的净成本可接受时。您可以通过从总成本中减去总收益来确定净成本或净收益。不安全解决方案的好处是“不会浪费时间进行不必要的运行时检查以确保正确性”;代价是 (1) 即使托管安全系统关闭,也必须编写安全的代码,以及 (2) 必须处理可能降低垃圾收集器效率的问题,因为它不能在具有非托管指针的内存中移动它。

或者,如果您是编写编组层的人。

仅仅是出于性能原因吗?

出于性能以外的原因在托管语言中使用指针似乎是不正当的。

在绝大多数情况下,您可以使用 Marshal 类中的方法来处理与非托管代码的互操作。 (在某些情况下,使用编组设备解决互操作问题可能很困难或不可能,但我不知道。)

当然,正如我所说,如果您是编写 Marshal 类的人,那么显然您无法使用编组层来解决您的问题。在这种情况下,您需要使用指针来实现它。

为什么 C# 会通过不安全的上下文公开此功能,并从中删除所有托管优势?

这些托管优势伴随着性能成本。例如,每次向数组询问第十个元素时,运行时都需要检查是否有第十个元素,如果没有则抛出异常。使用指针可以消除运行时成本。

相应的开发人员成本是,如果您做错了,那么您将处理内存损坏错误,这些错误会格式化您的硬盘并在一小时后使您的进程崩溃,而不是在错误发生时处理一个干净的异常。

理论上是否可以在不失去托管环境的任何优势的情况下使用指针?

我认为“优势”是指垃圾收集、类型安全和引用完整性等优势。因此,您的问题本质上是“理论上是否有可能关闭安全系统,但仍然可以获得打开安全系统的好处?”不,显然不是。如果您因为不喜欢它的昂贵而关闭该安全系统,那么您将无法获得开启它的好处!

【讨论】:

感谢 Eric 的回复。你能告诉我“参照完整性”是什么意思吗?是使用引用而不是指针吗? @Joan:每个引用实际上都指向 something valid 或者是 null。指针不具有该属性;指针可以引用根本没有任何好处的内存。但是托管引用具有该属性;如果您有对字符串的引用,则该内容始终为空或有效字符串;保证您不会处于对无效字符串的非空引用的情况。 谢谢 Eric,我现在明白了。 @masoudkeshavarz:不。使用托管指针,不可能伪造指向任意内存的指针。对于不安全代码中的非托管指针,我们只是说它们被称为“非托管”和“不安全”出于某种原因。您可以在不安全的代码中使用非托管指针做任何您想做的事情,包括破坏 .NET 运行时数据结构。 天哪,一个小时来一直在寻找一个明确的无答案的答案,这太棒了。谢谢!【参考方案2】:

指针与托管的垃圾收集环境存在内在矛盾。 一旦你开始搞乱原始指针,GC 就不知道发生了什么。

具体来说,它无法判断对象是否可访问,因为它不知道您的指针在哪里。 它也不能在内存中移动对象,因为这会破坏你的指针。

所有这些都将通过 GC-tracked 指针解决;这就是引用。

您应该只在混乱的高级互操作场景或高度复杂的优化中使用指针。 如果你不得不问,你可能不应该问。

【讨论】:

+1 for 如果你必须问,你可能不应该。很好的建议:-) 你的结论是对的,但你的大部分解释是错误的。从垃圾收集器的角度来看,指针和引用没有什么不同。破坏 GC 的是指针或引用存储在无类型内存区域中时,因为 GC 不再知道它只是一个数值还是托管对象的地址。 @SLaks:我没有说引用和指针没有区别,我说它们没有区别从垃圾收集器的角度来看。 GC 不在乎您是获取数组元素的地址,还是从指向不同元素的指针开始并进行算术运算以找到您现在指向的那个。 @SLaks:即使在本机 C 和 C++ 中,指针运算也只允许在单个对象/分配(例如数组)的范围内。垃圾收集器无论如何都会将整个对象一起移动,指针不会中断。 @SLaks:相当多。顺便说一句,您假设的 GC 跟踪指针确实存在于其他 .NET 语言中(尽管有一些限制——它只能是一个自动变量),并且它确实支持算术:interior_ptr【参考方案3】:

GC 可以移动引用;使用 unsafe 使对象不受 GC 控制,并避免了这种情况。 “固定”固定对象,但让 GC 管理内存。

根据定义,如果你有一个指向对象地址的指针,而 GC 移动了它,你的指针就不再有效。

关于为什么需要指针:主要原因是使用非托管 DLL,例如那些用 C++ 编写的

另请注意,当您固定变量并使用指针时,您更容易受到堆碎片的影响。

编辑

您已经谈到了托管代码与非托管代码的核心问题......内存是如何释放的?

您可以按照您的描述混合代码以提高性能,只是不能使用指针跨越托管/非托管边界(即,您不能在“不安全”上下文之外使用指针)。

至于它们是如何被清理的……你必须管理自己的记忆;您的指针指向的对象是使用(希望)CoTaskMemAlloc() 创建/分配的(通常在 C++ DLL 中),并且您必须以相同的方式释放该内存,调用 CoTaskMemFree(),否则您将有内存泄漏.请注意,只有使用CoTaskMemAlloc() 分配的内存才能使用CoTaskMemFree() 释放。

另一种选择是从您的本机 C++ dll 中公开一个方法,该方法接受一个指针并释放它...这让 DLL 决定如何释放内存,如果它使用其他方法来分配内存,则效果最好。您使用的大多数本机 dll 都是您无法修改的第三方 dll,而且它们通常没有(我见过的)此类可调用的函数。

释放内存的例子,取自here:

string[] array = new string[2];
array[0] = "hello";
array[1] = "world";
IntPtr ptr = test(array);
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
System.Console.WriteLine(result);

更多阅读材料:

C# deallocate memory referenced by IntPtr 下面的第二个答案解释了不同的分配/解除分配方法

How to free IntPtr in C#? 加强了以与分配内存相同的方式解除分配的需要

http://msdn.microsoft.com/en-us/library/aa366533%28VS.85%29.aspx 有关分配和释放内存的各种方法的官方 MSDN 文档。

简而言之...您需要知道内存是如何分配的才能释放它。

编辑 如果我正确理解您的问题,简短的回答是肯定的,您可以将数据交给非托管指针,在不安全的上下文中使用它,并在退出不安全的上下文后使数据可用。

关键是您必须使用fixed 块固定您引用的托管对象。这可以防止您引用的内存在 unsafe 块中被 GC 移动。这里涉及许多微妙之处,例如您不能重新分配在固定块中初始化的指针...如果您真的打算管理自己的代码,则应该阅读不安全和固定的语句。

总而言之,管理您自己的对象和以您描述的方式使用指针的好处可能不会像您想象的那样为您带来性能提升。为什么不这样做的原因:

    C# 非常优化且速度非常快 您的指针代码仍以 IL 形式生成,必须对其进行 jitted(此时需要进一步优化) 您并没有关闭垃圾收集器...您只是将您正在使用的对象排除在 GC 的权限之外。因此,每隔 100 毫秒左右,GC仍然会中断您的代码并为托管代码中的所有其他变量执行其函数。

HTH, 詹姆斯

【讨论】:

谢谢,但是当您使用指针时,完成后如何“清理”它们?是否可以在性能不佳的情况下使用它们,然后切换回托管代码? 感谢 James 提供更多信息。 @Joan:当然。但是有责任确保所有东西都被清理干净,没有指向可移动内存的杂散指针,等等。如果您想要关闭安全系统的好处,那么您必须承担安全系统通常为您做的事情的成本。 谢谢埃里克,这很有道理。但是在通过指针进行性能优化的情况下,一旦他/她完成,仍然会将数据返回到托管世界,对吗?像托管数据 -> 非托管数据 -> 对此数据进行一些快速操作 -> 从该非托管数据创建托管数据 -> 清理非托管内存 -> 回到托管世界? 进一步说明,您可以使用GC.AddMemoryPressureGC.RemoveMemoryPressure 显式通知垃圾回收来自非托管内存的内存压力。您仍然需要自己释放内存,但这样垃圾收集器在做出调度决策时会考虑非托管内存。【参考方案4】:

在 C# 中显式使用指针的最常见原因:

执行对性能非常敏感的低级工作(如字符串操作), 与非托管 API 接口。

从 C# 中删除与指针相关的语法的原因(根据我的知识和观点——Jon Skeet 会更好地回答 B-))在大多数情况下被证明是多余的。

从语言设计的角度来看,一旦您通过垃圾收集器管理内存,您就必须对指针可以做什么和不可以做什么施加严格的限制。例如,使用指针指向对象的中间可能会给 GC 带来严重的问题。因此,一旦限制到位,您就可以省略额外的语法并以“自动”引用结束。

此外,在 C/C++ 中发现的超仁慈的方法是常见的错误来源。对于大多数情况下,微性能根本无关紧要,最好提供更严格的规则并约束开发人员以减少难以发现的错误。因此,对于常见的业务应用程序而言,.NET 和 Java 等所谓的“托管”环境比假定针对裸机工作的语言更适合。

【讨论】:

指针不会从 C# 中删除。也许您正在考虑使用 Java? 我并不是说 pointers 被删除了,而是额外的语法被删除了,即不必写 obj->Property, obj.Property 可以代替。将澄清我的答案。 @Ondrej: That wasn't removed either. 本是对的;在 C# 中取消引用指针时,您肯定必须使用箭头(和星号)。不要将 pointersreferences 混淆; C# 支持两者。 @Eric Lippert 嘿,是的。然而,将引用视为指针的子集,我选择了“指针”一词作为更通用的变体来解释引用的演变——以及“无指针”语言(它的“安全”部分是正确的)——来自普通的旧指针。【参考方案5】:

假设您想使用 IPC(共享内存)在 2 个应用程序之间进行通信,那么您可以将数据编组到内存并通过 Windows 消息传递或其他方式将此数据指针传递给其他应用程序。在接收应用程序时,您可以取回数据。

在将数据从 .NET 传输到传统 VB6 应用程序的情况下也很有用,其中您将数据编组到内存,使用 win msging 将指针传递给 VB6 应用程序,使用 VB6 copymemory() 从托管内存空间获取数据到 VB6应用非托管内存空间..

【讨论】:

以上是关于何时在 C#/.NET 中使用指针?的主要内容,如果未能解决你的问题,请参考以下文章

为啥/何时使用 `intptr_t` 在 C 中进行类型转换?

C ++:在向量中插入啥 - 指针或引用,向量何时复制它的元素?

何时应该使用 Pipes 或 gRPC 进行进程间通信(在 C# .NET Core 中)?

C#中的委托是啥?事件是否一种委托?

C ++何时返回指针[关闭]

C语言面试中的问题-指针和引用的使用场景?