There is a lot to unpack and obviously, those terms are not the same. So let's see what is the difference between C#, .NET, IL and the JIT-Compiler. To do so we start with a nice overview where we go from top to bottom to explain the differences.
C# / F# / ...
Let's begin with the part on the top: The languages themselves. Most prominent we have C# and we have for example F#. Both are just languages. That is all! It is the same as let's say English. A language is defined by a set of symbols (the alphabet) and a set of rules that generates the language (grammar). The more scientific explanation of automata theory is:
In automata theory, a formal language is a set of strings of symbols drawn from a finite alphabet. A formal language can be specified either by a set of rules (such as regular expressions or context-free grammar) that generates the language, or by a formal machine that accepts (recognizes) the language.
As English gives you some nice words and grammar it does not automatically give you any sentences. You have to create those sentences. And multiple sentences can carry meaning. The same applies to C#, F# and friends. These languages just give you the rules (keywords and grammar) but you have to build something with it.
That brings us to the next step, because that alone is not enough. The next step in the chain is a compiler. Your C# compiler takes your source code and generates an assembly. But what is in that assembly? Well still your instructions you wrote in your C# code but in a different format: the Intermediate Language sometimes also referred as Common Intermediate Language. The same does a F# compiler. It takes your F# code and emits IL instructions into an assembly.
Before I go into detail I amazing thing before: Because all of the .NET languages emit the same intermediate language, you can use an assembly that was written originally in C# in your F# code and vice versa. That is why it is called common intermediate language. As the term might suggest it is not the end of the line. But we have two major gains:
- We are closer to the underlying OS/hardware and all the languages emit that exact language. From now on we don't have to deal anymore with different stuff.
- That said it lays groundwork for further optimizations later on.
Not everything a language offers will end up in the IL code. Here is a small example:
public class MyClass
public readonly string ReadonlyText = "some text";
public string Text = "some text";
readonly qualifier (which is a C# language feature) tells the compiler to check whether a user changed a reference to the object. But guess what, the C# compiler does not emit any
readonly IL code.
readonly is purely used for static analysis and is neither used in IL or in the JIT phase. Let's have a look at the generate IL code:
IL_0001: ldstr "some text"
IL_0006: stfld string MyClass::ReadonlyText
IL_000c: ldstr "some text"
IL_0011: stfld string MyClass::Text
It is absolutely identical. I read from time to time that
readonly can improve performance. And the easy answer is: No it can not, because the IL code is the same. Another way to see this is to use reflection. You can reassign the reference of
readonly fields at runtime without any problem.
Back to the CIL. If you want you could also write CIL code directly removing the need for A C# or F# compiler. Here is small exert how to do so 😄.
To wrap this up: A compiler in the .NET world takes your code and emits "new" code in the form of the intermediate language. Different compiler emit different IL code. For example the F# compiler, in contrast to the C# compiler, can emit
tail. is used for tail-call optimization, which if you are not familiar with, I covered here. If you want to see this in action here you go.
All the stuff we looked at happen at compile time (besides you writing code ... that is called design time) so we will now go into the realm of the runtime and the JIT-compiler.
As I said earlier the Intermediate Language is just one step but not the end of the line. And that is when we first have to talk about the .NET runtime before we can come to what the JIT-compiler does.
.NET runtime (CLR)
The idea behind .NET CLR (common language runtime) is to write code once but run it anywhere but the intermediate language is just not enough. Someone has to translate those IL instructions into something the operating system can handle. And that is what the .NET runtime does. When you click on an executable assembly (exe) the machine knows to invoke the .NET runtime which than utilizes the JIT (more later). But wait a second how can we write independent code and on double-click the OS knows what to do? Short: It does not. Long: That is the magic of your C#/F# compiler. If you produce an executable assembly (aka exe) it also emits OS- and architecture-dependent code how to start the CLR, which then takes care of IL and stuff. For libraries of course that does not apply. So we can see the CLR has to be written for all platforms and architectures where our code should run.
Back to the JIT. The JIT is used by the CLR to emit native machine code instructions which will run on that specific operating system and architecture. There is a difference if you have Windows x86 vs x64.
The JIT does not only translate IL to native code but also puts it into memory so the next time the CLR can directly run the full-native code. Another job of the JIT is to optimize code even further. The C# compiler can only statically do that but the JIT has to the full picture and can do stuff the IL can't. For example loop unrolling.
for (int i = 0; i < 1024; i++)
for (int i = 0; i < 1024; i += 4)
Foo(i + 1);
Foo(i + 2);
Foo(i + 3);
The advantage of loop unrolling is that we can have less branch prediction misses (more info here), reducing the amount of machine commands and possible even execute the commands in parallel thanks to SIMD. There are not many ways to influence the JIT from C# code itself. There are things like
MethodImplOptions.AggressiveInling which tells the JIT that he should try to inline that the given function, but you can't force him to do so.
That brings us last but not least to .NET.
What is .NET then?
What we understand under .NET is basically all of the above said and more. If you are using functions like
Console.WriteLine that is of course also part of .NET. To be very specific such stuff is part of the BCL - Base Class Library. But also the C#/F#,... compiler is part of .NET as well as runtime components, debugger, command line tools and so on.
I hope I could give you a good overview over the differences between C#,F#, .NET and the underlying IL code and the JIT-compiler.