How To Embed Post Compiled Code Into A Powershell Script

My last post talked about how to embed a .NET assembly that has already been compiled into a POSH script. This time I will talk about how to embed code for a .NET assembly into a script that will be compiled when the script is executed.

The .NET Framework comes with compilers for Visual Basic .NET and C# .NET. The .NET Framework also abstracts those compilers in the System.dll assembly through the Microsoft.VisualBasic.VBCodeProvider and Microsoft.CSharp.CSharpCodeProvider classes respectively.

Using the CSharpCodeProvider class, I have created two scripts that make it easy to insert C# code into a POSH script. The Invoke-CScript script will compile it’s code argument as a single method of a class and execute that method. The New-CAssembly script gives more direct access to the C# compiler allowing multiple, entire classes to be compiled. Once an assembly is compiled, it is loaded into the current application domain and reused whenever the scripts are invoked with an identical code parameter.

Precompiled and post compiled assemblies have advantages and disadvantages.

  • A precompiled assembly is not dependent on availability of compilers, while a post compiled assembly must be written in a language that the end user has a compiler for. This shouldn’t be a problem, however, if you program in Visual Basic or C#.
  • A precompiled assembly should load faster than the equivalent post compiled assembly since the precompiled assembly does not need to be processed by a compiler. This will only have a performance impact when the assembly is first loaded and will only be noticeable with very large assemblies.
  • A post compiled assembly can be inserted into a script as plain text, while a precompiled assembly must be maintained and compiled separately from the script.

Do not over use embedded assemblies. Both methods of embedding an assembly into a script can result in a huge performance boost when hundreds of records must be processed. Both methods can also allow .NET classes to defined and used by the rest of the script. However, when no significant performance boost can be attained the result would only be an over complication due to having to use separate languages for compiled and interpreted code. The act of compiling the assembly will also take time.

I have written and executed the following script as an illustration of the potential performance boost and as an example of how to use the two scripts. The script contains three functions that will use different methods to find the sum of the elements of it’s input array.

$CompileTime = Measure-Command {New-CAssembly @"
namespace Test {
public class SpeedTestClass {
public static int GetSum(int[] arr) {
int sum = 0; foreach (int i in arr) {sum += i;} return sum;
}
}
}
"@}

"TestCModule compile time: " + $CompileTime.TotalMilliseconds

function TestCModule([int[]]$in) {
[test.speedtestclass]::GetSum($in)
}

function TestCMethod([int[]]$in) {
Invoke-CScript 'int sum = 0; foreach (Object o in args) {sum += (int)o;} return sum;' $in
}

function TestPSMethod([int[]]$in) {
[int]$sum = 0
$in | Foreach {$sum += $_}
$sum
}
[int[]]$arr = 1..10
"TestCModule(1..10): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms"
"TestCMethod(1..10): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms"
"TestPSMethod(1..10): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms"
''
[int[]]$arr = 1..100
"TestCModule(1..100): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms"
"TestCMethod(1..100): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms"
"TestPSMethod(1..100): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms"
''
[int[]]$arr = 1..1000
"TestCModule(1..1000): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms"
"TestCMethod(1..1000): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms"
"TestPSMethod(1..1000): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms"
''
[int[]]$arr = 1..10000
"TestCModule(1..10000): $((measure-command {TestCModule($arr)}).TotalMilliseconds)ms"
"TestCMethod(1..10000): $((measure-command {TestCMethod($arr)}).TotalMilliseconds)ms"
"TestPSMethod(1..10000): $((measure-command {TestPSMethod($arr)}).TotalMilliseconds)ms"

The result:

TestCModule compile time: 184.6108
TestCModule(1..10): 4.0505ms
TestCMethod(1..10): 146.3526ms
TestPSMethod(1..10): 2.8179ms

TestCModule(1..100): 1.1498ms
TestCMethod(1..100): 25.9426ms
TestPSMethod(1..100): 14.3305ms

TestCModule(1..1000): 1.1065ms
TestCMethod(1..1000): 25.8739ms
TestPSMethod(1..1000): 187.4232ms

TestCModule(1..10000): 4.1893ms
TestCMethod(1..10000): 37.7209ms
TestPSMethod(1..10000): 1589.6593ms

The results of that test show that the code that made use of the class defined by the New-CAssembly compleated in just a couple of milliseconds. However, that is only after taking 184 milliseconds to compile that class. The code that used the Invoke-CScript returned in about 30 milliseconds in all but the first test. The large difference between using New-CAssembly and Invoke-CScript is that Invoke-CScript must make assurances that when the code is reused only the correct code is used, while with New-CAssembly we can simply create a unique type name and be fairly sure that when that type name is used we will be using the code that we want. The code that calculated the sum using Powershell syntax quickly began to take more time to process as the number of elements increased.

Although it would appear that Invoke-CScript has a large overhead in comparison to New-CAssembly, in reality this overhead is due to having to make a call to New-CAssembly to verify the code that get executed. When you just want to execute fast code on a large number of records then using Invoke-CScript would probably be the best route, if you can suffice with a single invocation.

# New-CAssembly.ps1
# Version: 1.0
# Author: LunaticExperimentalist
# End User License: Public Domain

param ([String]$Code = $(throw "C# code required to compile new assembly."))

# get code hash
$CodeHash = $Code.GetHashCode() -band 0x7fffffff

# look for an existing assembly
$ExistingAsm = [AppDomain]::CurrentDomain.GetAssemblies() | Foreach {
$InfoType = $_.GetType("EmitedInfoClass")
if (($InfoType) -and
($InfoType.GetProperty("ModuleCode").GetValue($null,$null) -eq $Code )) {
return $_
}
}

if ($ExistingAsm) {
# use existing assembly
return $ExistingAsm
}

# or compile a new assembly
$CSharpCodeProvider = New-Object Microsoft.CSharp.CSharpCodeProvider
$Parameters = New-Object System.CodeDom.Compiler.CompilerParameters(
@([AppDomain]::CurrentDomain.GetAssemblies() | where {$_.GlobalAssemblyCache -eq $true} | Foreach {$_.Location}),
$null,$false)
$Parameters.GenerateInMemory = $true
$CompileResult=$CSharpCodeProvider.CompileAssemblyFromDom($Parameters, @(
New-Object System.CodeDom.CodeSnippetCompileUnit(
'public class EmitedInfoClass { public static string ModuleCode{get{return "' +
($Code -replace '\','\' -replace '"','"' -replace 'r','r' -replace 'n','n') + '";} } }'
);
New-Object System.CodeDom.CodeSnippetCompileUnit($Code)
) )

# check for errors
if ($CompileResult.Errors.Count -eq 0) {
# use new assembly
$CompileResult.CompiledAssembly
}
else {
# write errors
$CompileResult.Errors | Foreach {
Write-Error "C Sharp Compile Error: Line $($_.Line) Col $($_.Column) `"$($_.ErrorText)`""
}
}
# Invoke-CScript.ps1
# Version: 1.0
# Author: LunaticExperimentalist
# End User License: Public Domain

param ([String]$Code = $(throw "C# code required to compile new assembly."), [Object]$Arguments = $null)

$Asm = New-CAssembly @"
using System;
namespace Emited {
public class EmitedClass {
public static Object EmitedCMethod(
Object[] args,
System.Management.Automation.PSObject[] input,
System.Management.Automation.EngineIntrinsics context)
{ $Code }
}
}
"@

if ($Asm) {
# use assembly
$method = $Asm.GetType("Emited.EmitedClass").GetMethod("EmitedCMethod")
$InputList = New-Object System.Collections.ArrayList
$Input | Foreach {[Void]$InputList.Add($_)}
$method.Invoke($null, @(@($Arguments),[System.Management.Automation.PSObject[]]$InputList.ToArray(),$ExecutionContext))
}

Advertisements

~ by lunaticexperimentalist on September 25, 2007.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: