Skip to main content

performance tuning - A Simplistic C# (.NET) Reloader


Can one write a very straightforward C# reloader akin to this Java reloader, that enables Mathematica-like usage of functions from existing C# code in string format, in an easy and elegant fashion directly in Mathematica?



Answer



Yes it is possible to write such a code and I will present the code and sample usage below. The motivation for this question/answer is this Java reloader by Leonid Shifrin and I borrowed some code from his answer.



Simplistic C# Reloader


The code below is a C# reloader. It takes a string of C# code and (optionally) a C# compiler path, attempts a compile and if the compilation succeeds it calls on NETLink to load the DLL. It also takes the same Options as LoadNETType, making it easy to choose how you want the loaded functions to appear.


Code


BeginPackage["DotNETReloader`", {"NETLink`"}];

dotNETLoader::usage =
"dotNETLoader[code_, compilerPath_] takes a C# code in string format
and (optionally) a C# compiler path, it then attempts to compile the
string code into a .NET dll suitable for use with NETLink"


dotNETLoader::dirmakeerr = "Can not create directory `1`";
dotNETLoader::invst = "The loader is not in a valid state. Perhaps some temporary
directories do not exist or could not be created";
dotNETLoader::cmperr = "The following compilation errors were encountered: \n`1`";

Options[dotNETLoader] = Options[LoadNETType]

Begin["`Private`"]

$stateValidQ = True;

$csc = If[$SystemID === "Windows-x86-64", "csc.exe", "mcs"];
$err = If[$SystemID === "Windows-x86-64", 2, 3];
$tempDirectory = FileNameJoin[{$UserBaseDirectory, "Temp", "dotNET"}];

(* If a namespace exists, get the name *)
getNamespace[code_String] :=
With[{ns =
StringCases[code, "namespace" ~~ Whitespace ~~ name : (WordCharacter ..) :> name]},
First @ ns /; ns =!= {}]


(* Get the class name *)
getClass[classCode_String] :=
With[{cl =
StringCases[classCode,
"public" ~~ Whitespace | (Whitespace ~~ "static" ~~ Whitespace) ~~
"class" | "struct" | "interface" ~~ Whitespace ~~
name : (WordCharacter ..) :> name]}, First @ cl /; cl =!= {}]

getClass[__] := Throw[$Failed, error[getClass]];


If[!FileExistsQ[#] && CreateDirectory[#] === $Failed,
Message[dotNETLoader::dirmakeerr, #]; $stateValidQ = False;] &@$tempDirectory

dotNETLoader[code_String, compiler_String: $csc,
opts : OptionsPattern[]] /; $stateValidQ :=
Module[{sourcePath, run,
options = FilterRules[{opts}, Options[dotNETLoader]],
namespace = getNamespace[code], className = getClass[code]},
sourcePath = FileNameJoin[{$tempDirectory, className <> ".cs"}];
Export[sourcePath, code, "String"];

run = RunProcess[{compiler, "-t:library", "-optimize+", sourcePath},
ProcessDirectory -> $tempDirectory];
If[run[[1]] =!= 0, Message[dotNETLoader::cmperr,
Style[#, Red]& @ run[[$err]]]; $Failed,
ReinstallNET[];
LoadNETAssembly @ FileNameJoin[{$tempDirectory, className <> ".dll"}];
If[Head @ namespace =!= getNamespace,
LoadNETType[namespace <> "." <> className, options],
LoadNETType[className, options]]
]

]

dotNETLoader[_String] := CompoundExpression[Message[dotNETLoader::invst]; $Failed]

End[]

EndPackage[]

Notes


I have tested this code successfully on both Windows and Linux and would appreciate it if someone that has a Mac can also test it on MacOS (I won't be able to do this but I expect it to work unaltered since the .NET platform and C# compiler are available via Mono). Also, please I will like others to test this code and provide feedback on all platforms. You will notice that I have named the function dotNETLoader instead of cSharpLoader, this is because I'm planning on extending this function to include F# (which I have successfully tested on Windows) and possibly, other .NET languages.



Due to my usage of RunProcess to avoid bringing up the console in Windows, this will only work on Mathematica 10 and above versions.


The package works by saving the C# string code and saving it as a .cs file in a temporary directory and using the New in 10 function RunProcess to invoke your installed C# compiler to compile the class into a .NET library .dll file. It then calls NETLink to load the library and make the functions available for use in Mathematica.


Usage


I will show some examples of how to use this package. Let's start by borrowing some C# codes from the web and saving them as strings.


code1 = 

"public class LCS{
public static string longestCommonSubstring(string s1, string s2)
{
int Start = 0;

int Max = 0;
for (int i = 0; i < s1.Length; i++){
for (int j = 0; j < s2.Length; j++){
int x = 0;
while (s1[i + x] == s2[j + x]){
x++;
if (((i + x) >= s1.Length || ((j + x) >= s2.Length)))
break;
}
if (x > Max) {

Max = x;
Start = i;
}
}
}
return s1.Substring(Start, Max);
}
}";

This code is the C# equivalent of the 2nd code from here (There are only a few differences but it took less than a minute to make the changes to C# code)



First we load the package:


Needs["DotNETReloader`"]

Now we load the function using dotNETLoader:


dotNETLoader[code1, StaticsVisible -> True]


NETType["LCS", 1]



We can see the possible options:



Options[dotNETLoader]


{StaticsVisible -> False, AllowShortContext -> True}



Setting StaticsVisible to True enables us to call the function directly without the class name and makes it feel like a built-in function.


longestCommonSubstring["AAABBBBCCCCC","CCCBBBAAABABA"]


"AAAB"




I'll add the same benchmark Leonid used in his Java reloader just to show that in some cases it may be better to use C#:


s1 = StringJoin@RandomChoice[{"A", "C", "T", "G"}, 10000];
s2 = StringJoin@RandomChoice[{"A", "C", "T", "G"}, 10000];

Here we use the Mathematica's built-in function LongestCommonSubsequence:


LongestCommonSubsequence[s1, s2] // RepeatedTiming    


{0.25, "GTCCCTCACACTG"}




And here is our loaded C# function:


longestCommonSubstring[s1, s2] // RepeatedTiming


{0.35, "GTCCCTCACACTG"}



This collatz optimization question is interesting, so I decided to see how our new C# reloader can help us here. I found this code online a while ago (can't remember where) and made just one simple change to parallelize it.


code2 = 


"using System.Threading.Tasks;

namespace Tester
{
public struct Calc
{
public static int[] ParallelCollatz(int[] list)
{
Parallel.For(0, list.Length, i =>
{

int count;
long k = list[i];
for (count = 1; k != 1; count++)
{
k = ((k & 1) == 0) ? k / 2 : 3 * k + 1;
}
list[i] = count;
});

return list;

}
}

}";

As before (load the package if not already loaded), we load the function using dotNETLoader


dotNETLoader[code2, StaticsVisible -> True]


NETType["Tester.Calc", 2]




I wanted to point out a few things about the package with this example. First, notice that the type is Tester.Calc instead of just Calc, this is becaude this class was placed in a namespace, but the reloader notices this and takes care of it for us. Secondly, the way we use the feature in conjuction with built-in Mathematica functions to solve the problem shows how natural the approach becomes. Finally, this will also work for structs as well as static classes. Back to the example:


int = Range[1*^6];   

(* Here ParallelCollatz is the loaded C# function *)

RepeatedTiming @ Ordering[ParallelCollatz @ int, -1]


{0.0610, {837799}}




I will compare this to the compiled-to-C function provided by DumpsterDoofus.


Ordering[collatzLength @ int, -1] // RepeatedTiming


{0.877, {837799}}



Summary


We see that this C# Reloader can be quite useful and fits naturally into the Mathematica-style usage scenario. In the first example the highly optimized built in function was only slightly faster than our borrowed-from-web code. What's interesting from my testing so far is that there's not much performance difference when using these functions on both Windows and Linux. For the second example where I compared brute force approaches, we see that the loaded function was over a magnitude faster.


Comments

Popular posts from this blog

plotting - How to draw lines between specified dots on ListPlot?

I would like to create a plot where I have unconnected dots and some connected. So far, I have figured out how to draw the dots. My code is the following: ListPlot[{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {1, 4}, {2, 5}, {3, 6}, {4, 7}, {1, 7}, {2, 8}, {3, 9}, {4, 10}, {1, 10}, {2, 11}, {3, 12}, {4,13}, {2.5, 7}}, Ticks -> {{1, 2, 3, 4}, None}, AxesStyle -> Thin, TicksStyle -> Directive[Black, Bold, 12], Mesh -> Full] I have thought using ListLinePlot command, but I don't know how to specify to the command to draw only selected lines between the dots. Do have any suggestions/hints on how to do that? Thank you. Answer One possibility would be to use Epilog with Line : ListPlot[ {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {1, 4}, {2, 5}, {3, 6}, {4, 7}, {1, 7}, {2, 8}, {3, 9}, {4, 10}, {1, 10}, {2, 11}, {3, 12}, {4, 13}, {2.5, 7}}, Ticks -> {{1, 2, 3, 4}, None}, AxesStyle -> Thin, TicksStyle -> Directive[Black, Bold, 12], Mesh -> Full, Epilog -> { Line[ ...

equation solving - Invert and fit implicitly defined curve

I need to fit an implicitly defined curve. I thought I could get some data out of Solve , and then using FindFit . Therefore, I would like to find the relation the parametric curve defined by $F(x,y)=0$: Solve[-(1/2) + 1/2 (0.41202 BesselK[0, 0.1 Sqrt[x^2 + y^2]] + (0.101483 x BesselK[1, 0.1 Sqrt[x^2 + y^2]])/Sqrt[x^2 + y^2]) == 0, y] But I can't get an output: Solve was unable to solve the system with inexact coefficients or the system obtained by direct rationalization of inexact numbers present in the system. Since many of the methods used by Solve require exact input, providing Solve with an exact version of the system may help. >> Edit: In particular, I would like to fit the data coming from the curve with the expression of another curve, and not with a function $f(x)$. In particular, since this clearly looks like a cardioid , I would like it to fit to something like it. What other strategies could I try?

dynamic - How can I make a clickable ArrayPlot that returns input?

I would like to create a dynamic ArrayPlot so that the rectangles, when clicked, provide the input. Can I use ArrayPlot for this? Or is there something else I should have to use? Answer ArrayPlot is much more than just a simple array like Grid : it represents a ranged 2D dataset, and its visualization can be finetuned by options like DataReversed and DataRange . These features make it quite complicated to reproduce the same layout and order with Grid . Here I offer AnnotatedArrayPlot which comes in handy when your dataset is more than just a flat 2D array. The dynamic interface allows highlighting individual cells and possibly interacting with them. AnnotatedArrayPlot works the same way as ArrayPlot and accepts the same options plus Enabled , HighlightCoordinates , HighlightStyle and HighlightElementFunction . data = {{Missing["HasSomeMoreData"], GrayLevel[ 1], {RGBColor[0, 1, 1], RGBColor[0, 0, 1], GrayLevel[1]}, RGBColor[0, 1, 0]}, {GrayLevel[0], GrayLevel...