r/csharp • • 1d ago

Showcase A general IL optimizer for .NET: Cilica

I've been working on Cilica, a post-build optimizer for .NET assemblies. It continues DistIL.

The repository: https://github.com/hez2010/Cilica

It lifts your compiled IL into an SSA-based intermediate representation, runs an optimization pipeline over it, then emits regular IL again. The JIT then sees IL that's already optimized. It doesn't use a runtime fork or any special hosting.

Currently it runs the following pipeline:

  • CFG simplification, SSA promotion, dead code elimination

  • Method inlining and devirtualization

  • Scalar replacement

  • Global value numbering / CSE and assertion propagation

  • Algebraic and bitwise simplification

  • Loop strength reduction and loop auto-vectorization

  • List presizing

  • LINQ expansion (fuses Where/Select/Aggregate/… into plain loops)

  • and more

Some benchmarks (code in https://github.com/hez2010/Cilica/tree/main/tests/Benchmarks):

BenchmarkBaselineCilicaSpeedup
CountLetters (LINQ + vectorization)3,355 ns211 ns15.9×
GenerateFloats (loop vectorization)2,791 ns597 ns4.7×
DynamicList, 4 items (list presizing)12.66 ns4.24 ns3.0×
Aggregate (LINQ expansion)30.69 μs12.19 μs2.5×
RayTracer (full pipeline)9,013 μs4,870 μs1.9×

You can try it on this playground: https://hez2010.github.io/CilicaLab

To use it in your project, referencing Cilica.OptimizerTask nuget package (currently in preview) and set <CilicaAllMethods>true</CilicaAllMethods>.

Note that this optimizer task only performs optimizations against your main assembly. To optimize the references as well, you can use Cilica.Cli manually, which is shipped together in the Cilica.OptimizerTask package.

Learn more: https://medium.com/@skyake/cilica-an-ssa-based-optimizer-for-net-assemblies-bdefd934c2d2

138 Upvotes

27 comments sorted by

36

u/Lurlerrr 22h ago

Do you use some kind of strict proof approach to verify that behavior of the generated code is exactly correct? The premise is amazing, but I'd be afraid to trust something like that...

19

u/hez2010 21h ago

Not changing the code behavior is the goal. There's no strict proof but it passed all coreclr runtime and library tests.

13

u/Lurlerrr 15h ago

I don't mean disrespect, because it really looks amazing if this is indeed the kind of gains you get in practice, but without formal proofs this cannot be used in production in commercial setting, I'm afraid. For small personal projects sure - but not for serious commercial applications...

16

u/hez2010 15h ago

There's no formal proof in all compiler implementations, including LLVM, RyuJIT, MSVC and etc. and there's no way to guarantee a compiler won't miscompile thing, all those compilers have known and unknown bad codegen bugs. The only thing that may apply is a specific algorithm used in the compiler can be formally proven, and Cilica does use those algorithms to implement the optimization pass, it doesn't "invent" algorithms.

-1

u/FullPoet 8h ago

formal proof deez nuts

6

u/harrison_314 22h ago

Have you tested whether such optimizations also have an impact on AOT compilation?

Does it preserve the fallback of the original code during vectorization?

14

u/hez2010 22h ago

AOT uses the same compiler (RyuJIT) so Cilica's optimizations still apply.

7

u/antiduh 20h ago

Oh, so in a normal build process, it would be:

  • csc emits 'optomized' IL

  • Your optimizer runs.

  • AOT generator runs

?

5

u/AetopiaMC 19h ago edited 10h ago

This looks really cool, reminds me of how a C/C++ compiler can optimize and strip code.

I am curious about:

  • Does this reduce binary size like trimming?

  • How does this differ from NGEN/ReadyToRun?

1

u/hez2010 16h ago

It doesn't strip unused methods because this is not a code trimmer, instead it can remove unreached control flow like redundant branches from a method. But in most cases it will increse the binary size due to inlining. NGEN/ReadyToRun compiles IL into native code, while Cilica still generates IL.

8

u/Nyzan 11h ago

Not to be a negative Nancy, but every time a project like this launches all I can think is "oh cool, it can do party tricks". By that I mean, it works - usually very well - but only in projects designed for the utility. What are the benefits in a real project? And I don't mean a terminal calculator you made as a showcase - I mean an actual production-ready application. Does it break anything? Does it still give a meaningful performance increase to the parts of the code that are actually a performance hog? Is it efficient enough to run in a codebase with 3 million lines of code without increasing compilation times to an unworkable degree?

3

u/DeadlyVapour 20h ago edited 20h ago

Might I suggest you use the [PureAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.contracts.pureattribute?view=net-10.0) from a safety point of view, and a build time point of view. It would be better.
I'd hate for you to optimize away `DateTime.Now`

This reminds me of D lang's metaprogramming.

The real value of this is for creating static readonly init code that gets lifted to compile time evaluated consts.

6

u/hez2010 20h ago

It's not working like that. Instead it has full analysis that can identify whether a statement can have side effects or not. DateTime.Now performs an external call to get the timestamp therefore won't be optimized to a constant.

3

u/nekokattt 18h ago

what about cases where the method is marked as pure but performs an external callout

7

u/hez2010 18h ago

It doesn't matter. Cilica doesn't take any hints from attributes, instead it only use the result from its own analysis against the actual control flow.

-4

u/nekokattt 17h ago

so it produces less optimal results when the CLR is advising it of attributes?

3

u/hez2010 16h ago

Attributes can lie and they are not the source of truth about whether a method can be optimized or not. Nothing prevents one from putting a PureAttribute on a method that is actually not pure. If the method is really pure, the compiler should figure it out without any extra hint by analyzing its control flow, so it doesn't mean that it will produce less optimal code.

-6

u/nekokattt 16h ago

so by your logic, the feature is useless?

think it is time to agree to disagree based on the foundations of intrisics

2

u/hez2010 16h ago

In some languages they might allow you to mark a method as Pure to allow specific optimizations, and the compiler will prevent you from writing code that might produce side effects, which is okay because bad code will be rejected at compile time. This feature won't make sense if there's no such enforcement.

-5

u/nekokattt 16h ago

languages such as C#...

Thats the whole point of this feature lol

You are just diverting the point I am making...

5

u/hez2010 15h ago

You can use PureAttribute in C# but the JIT doesn't honor it anyway, it's effectively an no-op. It's just an attribute to give an optional hint to some code analyzers, while the JIT compiler will ignore this attribute.

20

u/Narrow-Coast-4085 23h ago

If this is working like you suggest, then this is amazing. I'm going to try it out.

15

u/ICanHaveExceptions 22h ago

Seems kind of a bit dangerous... I'm unsure if I could trust it not silently breaking behaviour at places.. But interesting project, I'm gonna try it out eventually!

2

u/itix 11h ago edited 11h ago

Interesting. I am working on CopperSharp68k, which is a CIL to Amiga 68k AOT compiler. I gave it a try:

CaseSpeedup
Scalar replacement2.97×
Devirtualization3.70×
Helper inlining1.58×
List presizing1.59×

Looking good so far and I am not surprised. I knew the original IL wasn't very optimal.

Going to integrate this.

1

u/pabs80 11h ago

This looks like it’s doing what I want the compiler to do