了解不安全代码及其用途

Understanding Unsafe code and its uses

我目前正在按照一位以​​编程为生的朋友的建议阅读 ECMA-334。我在处理不安全代码的部分。虽然,我对他们在说什么感到有点困惑。

The garbage collector underlying C# might work by moving objects around in memory, but this motion is invisible to most C# developers. For developers who are generally content with automatic memory management but sometimes need fine-grained control or that extra bit of performance, C# provides the ability to write “unsafe” code. Such code can deal directly with pointer types and object addresses; however, C# requires the programmer to fix objects to temporarily prevent the garbage collector from moving them. This “unsafe” code feature is in fact a “safe” feature from the perspective of both developers and users. Unsafe code shall be clearly marked in the code with the modifier unsafe, so developers can't possibly use unsafe language features accidentally, and the compiler and the execution engine work together to ensure 26 8 9BLanguage overview that unsafe code cannot masquerade as safe code. These restrictions limit the use of unsafe code to situations in which the code is trusted.

例子

using System;
class Test
{
    static void WriteLocations(byte[] arr)
    {
        unsafe
        {
            fixed (byte* pArray = arr)
            {
                byte* pElem = pArray;
                for (int i = 0; i < arr.Length; i++)
                {
                    byte value = *pElem;
                    Console.WriteLine("arr[{0}] at 0x{1:X} is {2}",
                    i, (uint)pElem, value);
                    pElem++;
                }
            }
        }
    }
    static void Main()
    {
        byte[] arr = new byte[] { 1, 2, 3, 4, 5 };
        WriteLocations(arr);
        Console.ReadLine();
    }
}

shows an unsafe block in a method named WriteLocations that fixes an array instance and uses pointer manipulation to iterate over the elements. The index, value, and location of each array element are written to the console. One possible example of output is:

arr[0] at 0x8E0360 is 1
arr[1] at 0x8E0361 is 2
arr[2] at 0x8E0362 is 3
arr[3] at 0x8E0363 is 4
arr[4] at 0x8E0364 is 5

but, of course, the exact memory locations can be different in different executions of the application.

为什么知道这个数组的确切内存位置对我们开发人员有好处?有人可以在简化的上下文中解释这个理想吗?

我使用 fixed 的主要原因之一是为了与本机代码进行交互。假设您有一个具有以下签名的本机函数:

double cblas_ddot(int n, double* x, int incx, double* y, int incy);

您可以像这样编写互操作包装器:

public static extern double cblas_ddot(int n, [In] double[] x, int incx, 
                                       [In] double[] y, int incy);

并编写 C# 代码来调用它:

double[] x = ...
double[] y = ...
cblas_dot(n, x, 1, y, 1);

但现在假设我想对数组中间的一些数据进行操作,比如从 x[2] 和 y[2] 开始。不复制数组就无法调用。

double[] x = ...
double[] y = ...
cblas_dot(n, x[2], 1, y[2], 1);
             ^^^^
             this wouldn't compile

在这种情况下,fixed 可以派上用场。我们可以更改互操作的签名并使用来自调用者的 fixed。

public unsafe static extern double cblas_ddot(int n, [In] double* x, int incx, 
                                              [In] double* y, int incy);

double[] x = ...
double[] y = ...
fixed (double* pX = x, pY = y)
{
    cblas_dot(n, pX + 2, 1, pY + 2, 1);
}

我还在极少数情况下使用了 fixed,在这种情况下我需要对数组进行快速循环并且需要确保没有进行 .NET 数组边界检查。

fixed 语言功能不完全是 "beneficial",因为它是 "absolutely necessary"。

通常 C# 用户会将引用类型想象为等同于单间接指针(例如,对于 class Foo,此:Foo foo = new Foo(); 等同于此 C++:Foo* foo = new Foo();

实际上,C# 中的引用更接近于双向间接指针,它是一个指针(或者更确切地说,是一个句柄),指向一个大型对象中的条目 table,然后存储对象的实际地址。 GC 不仅会清理未使用的对象,还会 also move objects around in memory 避免内存碎片。

如果您只在 C# 中使用对象引用,那么这一切都很好。一旦你使用指针,你就会遇到问题,因为 GC 可以 运行 在 任何时间点 及时,即使在紧循环执行期间,并且当 GC 运行s 你的程序的执行被冻结(这就是为什么 CLR 和 Java 不适合 table 硬实时应用程序 - 在某些情况下 GC 暂停可以持续几百毫秒)。

...由于这种固有行为(在代码执行期间移动对象),您需要防止移动该对象,因此 fixed 关键字指示 GC 不要移动该对象.

一个例子:

unsafe void Foo() {

    Byte[] safeArray = new Byte[ 50 ];
    safeArray[0] = 255;
    Byte* p = &safeArray[0];

    Console.WriteLine( "Array address: {0}", &safeArray );
    Console.WriteLine( "Pointer target: {0}", p );
    // These will both print "0x12340000".

    while( executeTightLoop() ) {
        Console.WriteLine( *p );
        // valid pointer dereferencing, will output "255".
    }

    // Pretend at this point that GC ran right here during execution. The safeArray object has been moved elsewhere in memory.

    Console.WriteLine( "Array address: {0}", &safeArray );
    Console.WriteLine( "Pointer target: {0}", p );
    // These two printed values will differ, demonstrating that p is invalid now.
    Console.WriteLine( *p )
    // the above code now prints garbage (if the memory has been reused by another allocation) or causes the program to crash (if it's in a memory page that has been released, an Access Violation)
}

因此,通过将 fixed 应用于 safeArray 对象,指针 p 将始终是有效指针,不会导致崩溃或处理垃圾数据。

旁注:fixed 的替代方法是使用 stackalloc,但这会将对象生命周期限制在函数的范围内。

一般来说,"unsafe" 块中的确切内存位置并不那么相关。

中所述,当您使用垃圾收集器管理的内存时,您需要确保您正在操作的数据不会被移动(使用 "fixed")。你通常在

时使用它
  • 你在一个循环中多次运行性能关键操作,并且操作原始字节结构足够快。
  • 您正在进行 interop 并且有一些非标准的数据编组需求。

在某些情况下,您正在使用不受垃圾收集器管理的内存,此类情况的一些示例是:

  • 在与非托管代码进行互操作时,它可用于防止反复来回编组数据,而是在更大粒度的块中进行一些工作,使用"raw bytes",或映射到这些原始字节的结构。
  • 在使用需要与 OS 共享的大缓冲区执行 低级 IO 时(例如 scatter/gather IO)。
  • 内存映射文件中创建特定结构时。例如,一个 B+Tree 有内存页面大小的节点,它存储在一个基于磁盘的文件中,你想将它分页到内存中。