waf / CSharpRepl

A command line C# REPL with syntax highlighting – explore the language, libraries and nuget packages interactively.
https://fuqua.io/CSharpRepl/
Mozilla Public License 2.0
2.92k stars 113 forks source link

Add "Execute on Release" feature #280

Open Emik03 opened 1 year ago

Emik03 commented 1 year ago

Feature Description

Proposal

You can already use F9 and CTRL+F9 to see the generated MSIL in Debug and Release, respectively.

However as of C# REPL 0.6.3, it is currently not possible to also evaluate code with Release optimizations.

Reasons this would be useful is quite simple: Being able to see the differences between optimized and unoptimized outputs, and using functions that only work with inlining.

Implementation

There are many binds on Enter, so introducing a toggle (e.g. F2) that switches from Debug to Release sounds like the way to go. The only problem with this idea is that perhaps a person may forget that they are in one or the other, as with any modal system it can be easy to get lost or to be unaware of the current state.

I would also personally add a CLI arg "--release" to start the REPL automatically in release mode.

Example

It is not possible to experiment with the way the following functions get optimized:

> Unsafe.SkipInit(out int i);                                                                      

> i
0

> string.Join(", ", stackalloc byte[10].ToArray())     
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
└── 🟡 Length: 28

> [MethodImpl(MethodImplOptions.AggressiveInlining)] 
  string CurrentMethodName() => MethodBase.GetCurrentMethod().Name;

> CurrentMethodName()                                
CurrentMethodName
└── 🟡 Length: 17

This seems to suggest that all of these functions are zero-initialized and have no amount of inlining, though of course this guarantee is dropped if we are to copy-paste this code into our IDE and run it in Release mode.

Edgecases

It is important to also consider the following edgecase.

> #if DEBUG
  void Foo() => throw new();
  #else
  void Foo() { }
  #endif

> Foo();

It should be expected that Foo throws when you run it in Debug and no-op (or even inlined) on Release. This means that you require two compilation contexts.

waf commented 1 year ago

This is an interesting idea. I just manually converted CSharpRepl to compile/evaluate everything in Release mode (OptimizationLevel.Release), and it looks like the optimizations you're expecting still don't take place. So it's possible that the underlying APIs we're using don't apply the same optimizations that we'd get in a "real" release mode. Needs a bit of digging I think.

Emik03 commented 1 year ago

Hi, turns out I was wrong about the behavior of the C# runtime.

The first one is explained by the lack of SkipLocalsInitAttribute. I forget that by default I have this enabled via [module: SkipLocalsInit].

image

The second one is explained by the fact that AggressiveInlining will still not inline your function if there is an observable difference. Since the method uses reflection to refer to itself, no inlining occurs.

That being said, is there a way to annotate attributes to the current module/assembly?

image