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

functions - Get leading series expansion term?

Given a function f[x] , I would like to have a function leadingSeries that returns just the leading term in the series around x=0 . For example: leadingSeries[(1/x + 2)/(4 + 1/x^2 + x)] x and leadingSeries[(1/x + 2 + (1 - 1/x^3)/4)/(4 + x)] -(1/(16 x^3)) Is there such a function in Mathematica? Or maybe one can implement it efficiently? EDIT I finally went with the following implementation, based on Carl Woll 's answer: lds[ex_,x_]:=( (ex/.x->(x+O[x]^2))/.SeriesData[U_,Z_,L_List,Mi_,Ma_,De_]:>SeriesData[U,Z,{L[[1]]},Mi,Mi+1,De]//Quiet//Normal) The advantage is, that this one also properly works with functions whose leading term is a constant: lds[Exp[x],x] 1 Answer Update 1 Updated to eliminate SeriesData and to not return additional terms Perhaps you could use: leadingSeries[expr_, x_] := Normal[expr /. x->(x+O[x]^2) /. a_List :> Take[a, 1]] Then for your examples: leadingSeries[(1/x + 2)/(4 + 1/x^2 + x), x] leadingSeries[Exp[x], x] leadingSeries[(1/x + 2 + (1 - 1/x...

mathematical optimization - Minimizing using indices, error: Part::pkspec1: The expression cannot be used as a part specification

I want to use Minimize where the variables to minimize are indices pointing into an array. Here a MWE that hopefully shows what my problem is. vars = u@# & /@ Range[3]; cons = Flatten@ { Table[(u[j] != #) & /@ vars[[j + 1 ;; -1]], {j, 1, 3 - 1}], 1 vec1 = {1, 2, 3}; vec2 = {1, 2, 3}; Minimize[{Total@((vec1[[#]] - vec2[[u[#]]])^2 & /@ Range[1, 3]), cons}, vars, Integers] The error I get: Part::pkspec1: The expression u[1] cannot be used as a part specification. >> Answer Ok, it seems that one can get around Mathematica trying to evaluate vec2[[u[1]]] too early by using the function Indexed[vec2,u[1]] . The working MWE would then look like the following: vars = u@# & /@ Range[3]; cons = Flatten@{ Table[(u[j] != #) & /@ vars[[j + 1 ;; -1]], {j, 1, 3 - 1}], 1 vec1 = {1, 2, 3}; vec2 = {1, 2, 3}; NMinimize[ {Total@((vec1[[#]] - Indexed[vec2, u[#]])^2 & /@ R...

How to remap graph properties?

Graph objects support both custom properties, which do not have special meanings, and standard properties, which may be used by some functions. When importing from formats such as GraphML, we usually get a result with custom properties. What is the simplest way to remap one property to another, e.g. to remap a custom property to a standard one so it can be used with various functions? Example: Let's get Zachary's karate club network with edge weights and vertex names from here: http://nexus.igraph.org/api/dataset_info?id=1&format=html g = Import[ "http://nexus.igraph.org/api/dataset?id=1&format=GraphML", {"ZIP", "karate.GraphML"}] I can remap "name" to VertexLabels and "weights" to EdgeWeight like this: sp[prop_][g_] := SetProperty[g, prop] g2 = g // sp[EdgeWeight -> (PropertyValue[{g, #}, "weight"] & /@ EdgeList[g])] // sp[VertexLabels -> (# -> PropertyValue[{g, #}, "name"]...