Performance, Caching and Concurrency - oleg-shilo/cs-script.net-framework GitHub Wiki
C# code executed with CS-Script can be extremely performant. However certain execution scenarios can lead to the situations when all benefits of compiled execution offered by CS-Script are completely lost.
The next two sections explain the internals of the script execution and also provide the guidance on how to chose the best most optimized and most maintainable execution model.
As a rule of thumb is "Rely on Caching and enable InMemoryAssembly loading".
Caching
The runtime performance of C# scripting solution is identical to the compiled application. The truly impressive execution speed is attributed to the fact that the script at runtime is just an ordinary CLR assembly, which just happens to be compiled on-fly.
However the compilation itself is a potential performance hit. Unfortunately, the compilation is a heavy task, initialization of the engine can bring an enormous startup overhead (e.g. Roslyn). And this is when caching comes to the rescue.
With caching a given script file is never recompiled unless it's changed since the last execution. This concept is extremely similar to the Python caching model.
The CS-Script sophisticated caching algorithm even partially supports file-less scripts (aka eval) execution. Thus in the code below two delegates are created but the code is actually compiled only once (what is not possible neither for Mono nor Roslyn).
var code = @"int Sqr(int a) { return a * a; }";
var sqr1 = CSScript.CodeDomEvaluator.CreateDelegate(code);
var sqr2 = CSScript.CodeDomEvaluator.CreateDelegate(code);
Note: Caching (as well as debugging) is only compatible with CodeDOM compiler engine: calls CSSCript.Load*
, CSScript.Compile.*
and CSScript.CodeDomEvaluator.*
). Thus caching will be effectively disabled for both CSScript.MonoEvaluator.*
and CSScript.RoslynEvaluator.*
.
But of course the major benefit of caching comes for script file execution.
Caching can be disabled:
- With
-c:0
switch from command line - With
CSScript.CacheEnabled = false
from code
However sometimes instead of disabling caching completely it may be more beneficial to perform fine caching control. This points will help you to understand some caching internals:
-
When script file is compiled CS-Script always creates assembly file, which is placed in the dedicated directory (cache) for further loading/execution. This is how CodeDOM compilation works. The benefit of file based assembly is that it can be cached and debugged. Something that neither Mono nor Roslyn compiler-as-service solutions can do.
-
The compiled assembly file is always created. Regardless if the caching is enabled or not. Enabling caching simply enables using the previous compilation result (assembly) if it is still up to date.
-
Location of the compiled script is deterministic and can be discovered by right-clicking the script file in explorer and selecting the corresponding option from the context menu. Alternatively the cached script location can be deducted from the script file location:
CSScript.GetCachedScriptPath("script full path");
A typical all cache data is placed in %temp%\CSSCRIPT\Cache\<scriptDirHash>
. When hosting CS-Script user can change the location of the all temporary data root folder:
CSSEnvironment.SetScriptTempDir("<some new location>");
After the call above the cache directory will be remapped as <some new location>\Cache\<scriptDirHash>
-
The cache directories also contain some extra temporary files that are needed for injecting script specific metadata into the script assembly. This metadata is used to allow script reflecting itself: Script Reflection.
-
Cache directory doesn't grow endlessly and it is of a fixed size. Any temp files that are no longer needed are always removed on script host exit event. Purging non-temporary but cached compiled scripts (e.g. if the source scripts do not exist any more) can be done by executing
cscs cache -trim
command. The script execution footprint on your system does not depend on the number of script executions but rather on the number of unique scripts present on the system and ever been executed. If it is detected that your cache is constantly growing then it needs to be reported as a defect so it can be fixed. -
Caching is not an obstruction but a help. There were a reports about cached files being locked by the executing process leading to compiler error "Access to ...cs.compiled file is denied". This sort of problems is always caused by another process changing the the script file and truing to compile while while the script assembly is still loaded for execution. This scenario is rare but not entirely unusual. It's important to understand that it is a logical problem not a technical one. And while disabling caching will prevent locking it is a very heavy price to be paid and it doesn't address the problem directly. The more practical and very reliable approach is to keep caching enabled but allow loading assembly as in-memory image leaving the compiled file completely unlocked (details are in the Concurrency section).
Concurrency
Any script execution may be a subject to some sort of synchronization in concurrent execution scenarios.
Note: synchronization (concurrency control) may only be required for execution of the same script by two or more competing processes. If one process executes script_a.cs
and another one executes script_b.cs
then there is no need for any synchronization as the script files are different and their executions do not collide with each other.
Hosted script code execution In case of hosted execution very often it is a script code (in-memory string) that is executed not the file. Thus there is no any competition for the same resources (e.g. script file) by concurrent executions. Mening that there is no need for any concurrency control.
Hosted script file execution In this case there is a common resource (script file). Thus script engine needs to synchronize the access to this resource with the other concurrent executions if any.
Standalone script file execution In this case the execution is also based on the shared resource (script file) and concurrency control is applicable.
The most critical stage of script execution is "Compilation" and it typically needs to be atomic and synchronized system wide. During this stage script engine compiles the script into assembly and any attempts to compile the same assembly at the same time will lead the error of the underlying compiler engine caused by the file locking (unless compilation attempts are synchronized).
In order to avoid this CS-Script uses global synchronization objects, which are used to by the competing engine instances for detecting when it is OK to do the compilation. Simply put, the script engine says "I am busy compiling this script. If you want to compile it too, wait until I am done.". Using caching (section above) dramatically decreases any possibility for an access collision by avoiding unnecessary compilations.
The concurrency model is controlled by the Settings.ConcurrencyControl
configuration object, which is set to Standard
by default.
- Standard: Simple model. The script engine doesn't start the timestamp validation and the script compilation until another engine validating finishes its job. Note: the compilation may be skipped if caching is enabled and the validation reviles that the previous compilation (cache) is still up to date. Due to the limited choices with the system wide named synchronization objects on Linux
Standard
is the only available synchronization model on Linux. Though it just happens to be a good default choice for Windows as well. - HighResolution: A legacy synchronization model available on Windows only. While it can be beneficial in the intense concurrent "border line" scenarios, its practical value very limited.
- None: No concurrency control is done by the script engine. All synchronization is the responsibility of the hosting environment.
Another stage of script execution is invoking the script entry point. This stage cannot and should not be synchronized as it is reflecting the business logic it may require the script to be loaded (active) indefinitely. This, in turn, can lead to the undesired locking of the compiled script until its execution is complete and the assembly file is released. This unpleasant practical implication(s) can be fully addressed by forcing script engine to load the compiled script not as a file but rather as it's in-memory image - InMemoryAssembly
mode. It can be enabled either:
- With
-inmem
command line switch - With
CSScript.GlobalSettings.InMemoryAssembly = true
from code - With InMemoryAssembly setting in ConfigConsole
InMemoryAssembly
is an enormously convenient mode that solves many concurrency problems in a very elegant way. The reason for InMemoryAssembly be set to false by default is rather historical. Foe years it has been a default loading model of .NET. But in the releases after v3.16 it will be set to true as a recognition of the new trend tarted with Roslyn, which unconditionally loads the assemblies as in-memory image.