Execution-Time Code Generation in C#

If you come from a C++ background, you may have a “compile-time” view of the world. Because a C++ compiler does all code generation when the code is compiled, C++ programs are static systems that are fully known at compile time.

The CLR provides a new way of doing things. The compile-time world still exists, but it’s also possible to build dynamic systems where new code is added by loading assemblies or even by writing custom code on the fly.

1. Loading Assemblies

In the .NET CLR, it’s possible to load an assembly from disk and to create instances of classes from that assembly. To demonstrate this, this chapter shows how to build a simple logging facility that can be extended by the customer at runtime to send informational messages elsewhere.

The first step is to define the standard part of the facility:

// file=LogDriver.cs

// compile with: csc /target:library LogDriver.cs

using System;

using System.Collections;

public interface ILogger

{

void Log(string message);

}

public class LogDriver

{

ArrayList loggers = new ArrayList();

public LogDriver()

{

}

public void AddLogger(ILogger logger)

{

loggers.Add(logger);

}

public void Log(string message)

{

foreach (ILogger logger in loggers)

{

logger.Log(message);

}

}

}

public class LogConsole: ILogger {

public void Log(string message)

{

Console.WriteLine(message);

}

}

In this step, you define the ILogger interface that your loggers will implement and the LogDriver class that calls all the registered loggers whenever the Log() function is called. Also, a LogConsole implementation logs messages to the console. This file is compiled to an assembly named LogDriver.dll.

In addition to this file, a small class exercises the loggers: using System;

class Test

{

public static void Main()

{

LogDriver logDriver = new LogDriver();

logDriver.AddLogger(new LogConsole());

logDriver.Log(“Log start: ” + DateTime.Now.ToString());

for (int i = 0; i < 5; i++)

{

logDriver.Log(“Operation: ” + i.ToString());

}

}

logDriver.Log(“Log end: ” + DateTime.Now.ToString());

}

This code merely creates a LogDriver, adds a LogConsole to the list of loggers, and does some logging.

2. Making It Dynamic

It’s now time to add some dynamic ability to the system. You’ll need a mechanism so the LogDriver class can discover there’s a new assembly that contains an additional logger. To keep the sample simple, the code will look for assemblies named LogAddIn*.dll.

The first step is to come up with another implementation of ILogger. The LogAddInToFile class logs messages to logger.log:

// file=LogAddInToFile.cs

// compile with: csc /r:..\logdriver.dll /target:library logaddintofile.cs

using System;

using System.Collections;

using System.IO;

public class LogAddInToFile: ILogger {

StreamWriter streamWriter;

public LogAddInToFile()

{

streamWriter = File.CreateText(@”logger.log”); streamWriter.AutoFlush = true;

}

public void Log(string message)

{

streamWriter.WriteLine(message);

}

}

This class doesn’t require much explanation. Next, you need to add the code to load the assembly to the LogDriver class:

void ScanDirectoryForLoggers()

{

DirectoryInfo dir = new DirectoryInfo(@”.”);

foreach (FileInfo f in dir.GetFiles(@”LogAddIn*.dll”))

{

ScanAssemblyForLoggers(f.FullName);

}

}

void ScanAssemblyForLoggers(string filename)

{

Assembly a = Assembly.LoadFrom(filename);

foreach (Type t in a.GetTypes())

{

if (t.GetInterface(“ILogger”) != null)

{

ILogger iLogger = (ILogger) Activator.CreateInstance(t);

loggers.Add(iLogger);

}

}

}

}

The ScanDirectoryForLoggers() function looks in the current directory for any files that match your specification. When one of the files is found, ScanAssemblyForLoggers() is called. This function loads the assembly and then iterates through each of the types contained in the assembly. If the type implements the ILogger interface, then an instance of the type is created using Activator.CreateInstance(), the instance is cast to the interface, and the interface is added to the list of loggers.

If you desire an even more dynamic implementation, you could use a FileChangeWatcher object to watch a specific directory, and you could then load any assemblies copied to that directory.

A few caveats exist with regard to loading assemblies from a disk. First, the runtime locks assemblies when they’re loaded. Second, it’s not possible to unload a single assembly, so if unloading an assembly is required (to update a class, for example), it will need to be loaded in a separate application domain since application domains can be unloaded. For more informa­tion on application domains, consult the .NET CLR documentation.

3. Custom Code Generation

It’s sometimes necessary for a class to have the best performance possible. For some algorithms, it’s easy to write a general solution to a problem, but the overhead of the general solution may be undesirable. A custom solution to the problem may be possible but can’t be generated ahead of time because the particulars of the problem aren’t known until the runtime.

In such situations, it may be useful to generate the custom solution at execution time. This technique is often known as self-modifying code.

3.1. Polynomial Evaluation

This section implements a polynomial evaluator for polynomials in the following form:

Y = anxn + … + a2x2 + alx + a0

To get rid of the exponentiation operation, which is slow, you can nicely rearrange the equation into this:

Y = a0 + x (al + x (a2 + … x (an) ) )

The first step in this exercise is to write the simple general solution to the problem. Since several solutions will exist, it will take a few files to build up a framework. The first is a utility class to do timing:

using System; namespace Polynomial {

class Counter {

public static long Frequency {

get

{

long freq = 0;

QueryPerformanceFrequency(ref freq);

return freq;

}

}

public static long Value {

get

{

long count = 0;

QueryPerformanceCounter(ref count);

return count;

}

}

[System.Runtime.InteropServices.DllImport(“KERNEL32”)]

private static extern bool

QueryPerformanceCounter( ref long lpPerformanceCount);

[System.Runtime.InteropServices.DllImport(“KERNEL32”)]

private static extern bool

QueryPerformanceFrequency( ref long lpFrequency);

}

}

The Counter class encapsulates the Win32 performance counter functions and can be used to get accurate timings. .NET 2.0 introduces a new class called Stopwatch that wraps the func­tionality of the Win32 performance counter. If an application requires precise time functionality, and it doesn’t require support for older framework versions, you should use Stopwatch over the Counter class.

Next, add a helper class to hold the information about Polynomial:

namespace Polynomial {

using System;

using PolyInterface;

/// <summary>

/// The abstract class all implementations inherit from

/// <Isummary>

public abstract class Polynomial {

public Polynomial(params double[] coefficients)

{

this.coefficients = new double[coefficients.Length];

for (int i = 0;

i < coefficients.Length; i++) this.coefficients[i] = coefficients[i];

}

public abstract double Evaluate(double value);

public abstract IPolynomial GetEvaluate();

protected double[] coefficients = null;

}

}

The Polynomial class is an abstract class that holds the polynomial coefficients.

A small interface defines the evaluation function:

namespace PolyInterface

{

/// <summary>

/// The interface that implementations will implement

/// <Isummary>

public interface IPolynomial {

double Evaluate(double value);

}

}

The following class implements the general method of evaluation:

namespace Polynomial {

using System;

/// <summary>

/// The simplest polynomial implementation III <Isummary>

/// <description>

/// This implementation loops through the coefficients and evaluates each

/// term of the polynomial.

/// <Idescription>

class PolySimple: Polynomial

{

public PolySimple(params double[] coefficients): base(coefficients)

{

}

public override IPolynomial GetEvaluate()

{

return((IPolynomial) this);

}

public override double Evaluate(double value)

{

double retval = coefficients[0];

double f = value;

for (int i = 1; i < coefficients.Length; i++)

{

retval += coefficients[i] * f;

f *= value;

}

return(retval);

}

}

}

This is a simple evaluator that merely walks through the polynomial term by term, accumu­lates the values, and returns the result.

Finally, the driver ties it all together:

namespace Polynomial

{

using System;

using System.Diagnostics; 

/// <summary>

/// Driver class for the project

/// </summary>

public class Driver
{

/// <summary>

/// Times the evaluation of a polynomial

/// </summary>

///The polynomial to evaluate

public static double TimeEvaluate(Polynomial p)

{

double value = 2.0;

Console.WriteLine(“{0}”, p.GetType().Name);

// Time the first iteration. This one is done

// separately so that we can figure out the startup

// overhead separately…

long start = Stopwatch.GetTimeStamp();

IPolynomial iPoly = p.GetEvaluate();

long delta = Stopwatch.GetTimeStamp() -start;

Console.WriteLine(“Overhead = {0:f2} seconds”,

(double) delta/ Stopwatch.Frequency);

Console.WriteLine(“Eval({0}) = {1}”, value, iPoly.Evaluate(value));

int limit = 100000;

Stopwatch timer = Stopwatch.StartNew();

// Evaluate the polynomial the required number of

// times.

double result = 0;

for (int i = 0; i < limit; i++)

{

result += iPoly.Evaluate(value);

}

timer.Stop();

double ips = (double)limit / (double)timer.ElapsedMilliseconds / 1000D;

Console.WriteLine(“Evalutions/Second = {0:f0}”, ips);

Console.WriteLine();

return(ips);

}

/// <summary>

/// Run all implementations for a given set of coefficients

/// </summary>

/// <param name=”coeff”> </param>

public static void Eval(double[] coeff)

{

Polynomial[] imps = new Polynomial []

{

new PolySimple(coeff),

}

double[] results = new double[imps.Length];

for (int index = 0;

index < imps.Length; index++)

{

results[index] = TimeEvaluate(imps[index]);

}

Console.WriteLine(“Results for length = {0}”, coeff.Length);

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

{

Console.WriteLine(“{0} = {1:f0}”, imps[index], results[index]);

}

Console.WriteLine();

}

/// <summary>

/// Main function.

/// </summary>

public static void Main()

{

Eval(new Double[] {5.5});

// Evaluate the second polynomial, with 7 elements double[] coeff = new double[] {5.5, 7.0, 15, 30, 500, 100, 1};

Eval(coeff);

// Evaluate the second polynomial, with 50 elements coeff = new double[50]; for (int index = 0; index < 50; index++)

{

coeff[index] = index;

}

Eval(coeff);

}

}

}

The TimeEvaluate() function takes a class that derives from Polynomial and calls GetEvaluate() to obtain the IPolynomial interface to do the evaluation. It times the GetEvaluate() function to determine the initialization overhead and then calls the evaluation function 100,000 times.

The driver evaluates polynomials with 1, 7, and 50 coefficients and writes out timing information.

The initial run generates the results in Table 32-1 (which are counts in evaluations/second).

Those results are really quite good, but it will be interesting to see if a custom solution can do better.

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 *