Practical DiskDiff in C#: Unsafe Context

Code verification has many benefits in the .NET runtime. Being able to verify that code is type-safe not only enables download scenarios but it also prevents many common programming errors.

When dealing with binary structures or talking to COM objects that take structures containing pointers, or when performance is critical, you’ll need more control. In these situations, you can use unsafe code.

Unsafe means that the runtime can’t verify the code is safe to execute. It therefore can be executed only if the assembly has full trust, which means it can’t be used in download scenarios, preventing abuse of unsafe code for malicious purposes.

The following is an example of using unsafe code to copy arrays of structures quickly. The structure being copied is a point structure consisting of x and y values.

Three versions of the function that clones arrays of points exist. ClonePointArray() is written without using unsafe features and merely copies the array entries. The second version, ClonePointArrayUnsafe(), uses pointers to iterate through the memory and copy it. The final version, ClonePointArrayMemcpy(), calls the system function CopyMemory() to perform the copy. To give some time comparisons, use the following code:

// file=unsafe.cs

// compile with: csc /unsafe /o+ unsafe.cs

using System;

using System.Diagnostics;

using System.Runtime.InteropServices;

class Counter {

public static long Frequency {

get

{

long freq = 0;

QueryPerformanceFrequency(ref freq);

return freq;

}

}

public static long Value {

get

{

long count = 0;

OueryPerformanceCounter(ref count);

return count;

}

}

[System.Runtime.InteropServices.DllImport(“KERNEL32”,

CharSet=System.Runtime.InteropServices.CharSet.Auto)]

private static extern bool QueryPerformanceCounter(

ref long lpPerformanceCount);

 

[System.Runtime.InteropServices.DllImport(“KERNEL32”,

CharSet=System.Runtime.InteropServices.CharSet.Auto)]

private static extern bool QueryPerformanceFrequency(

ref long lpFrequency);

}

public struct Point {

public Point(int x, int y)

{

this.x = x;

this.y = y;

}

// safe version

public static Point[] ClonePointArray(Point[] a)

{

Point[] ret = new Point[a.Length];

for (int index = 0; index < a.Length; index++)

ret[index] = a[index];

return(ret);

}

// unsafe version using pointer arithmetic

unsafe public static Point[] ClonePointArrayUnsafe(Point[] a)

{

Point[] ret = new Point[a.Length];

// a and ret are pinned; they cannot be moved by

// the garbage collector inside the fixed block.

fixed (Point* src = a, dest = ret)

{

Point*         pSrc = src;

Point*         pDest = dest;

for (int index = 0; index < a.Length; index++)

{

*pDest = *pSrc;

pSrc++;

pDest++;

}

}

return(ret);

}

// import CopyMemory from kernel32

[DllImport(“kernel32.dll”)]

unsafe public static extern void

CopyMemory(void* dest, void* src, int length);

 

// unsafe version calling CopyMemory()

unsafe public static Point[] ClonePointArrayMemcpy(Point[] a)

{

Point[] ret = new Point[a.Length];

fixed (Point* src = a, dest = ret)

{

CopyMemory(dest, src, a.Length * sizeof(Point));

}

return(ret);

}

public override string ToString()

{

return(String.Format(“({0}, {1})”, x, y));

}

int x;

int y;

}

class Test {

const int         iterations = 20000;                  //    #   to   do copy

const int         points = 1000;                        //    #   of   points in array

const int         retryCount = 5;                      //    #   of   times to retry

public delegate Point[] CloneFunction(Point[] a);

public static void TimeFunction(Point[] arr,

CloneFunction func, string label)

{

Point[] arrCopy = null; long start; long delta;

double min = 5000.0d;                // big number;

// do the whole copy retryCount times, find fastest time

for (int retry = 0; retry < retryCount; retry++)

{

start = Counter.Value;

for (int iterate = 0; iterate < iterations; iterate++)

arrCopy = func(arr);

delta = Counter.Value – start;

double result = (double) delta / Counter.Frequency;

if (result < min) min = result;

}

Console.WriteLine(“{0}: {1:F3} seconds”, label, min);

}

public static void Main()

{

Console.WriteLine(“Points, Iterations: {0} {1}”, points, iterations);

Point[] arr = new Point[points];

for (int index = 0; index < points; index++)

arr[index] = new Point(3, 5);

TimeFunction(arr,

new CloneFunction(Point.ClonePointArrayMemcpy), “Memcpy”);

TimeFunction(arr,

new CloneFunction(Point.ClonePointArrayUnsafe), “Unsafe”);

TimeFunction(arr,

new CloneFunction(Point.ClonePointArray), “Baseline”);

}

}

The timer function uses a delegate to describe the clone function, so it can use any of the clone functions. It uses a Counter class, which provides access to the system timers. The accuracy of this class will vary based upon the version of Windows that’s being used.

As with any benchmarking, the initial state of memory is important. To help control this, TimeFunction() executes each method five times and prints only the shortest time. Typically, the first iteration is slower; the CPU cache isn’t ready yet, so subsequent times get faster. The times listed in Table 38-1 were generated on a 600MHz Pentium III laptop running Windows 2000

Professional, but they were generated with beta software, so the performance probably isn’t indicative of the performance of the final product.

The program was run with several different values for points and iterations.

For small arrays, the unsafe code is the fastest, and for large arrays, the system call is the fastest. The system call loses on smaller arrays because of the overhead of calling into the native function. The interesting part is that the unsafe code isn’t a clear win over the baseline code.

The lessons in all this are that unsafe code doesn’t automatically mean faster code and it’s important to benchmark when doing performance work.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *