Why does the X86 for the following C# method CallViaStruct
include the cmp
instruction?
struct Struct {
public void NoOp() { }
}
struct StructDisptach {
Struct m_struct;
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
//push ebp
//mov ebp,esp
//cmp byte ptr [ecx],al
//pop ebp
//ret
}
}
Here is a more complete program that can be compiled with various (release) decompilations as comments. I expected the X86 for CallViaStruct
in both ClassDispatch
and StructDispatch
types to be the same however the version in StructDispatch
(extracted above) includes a cmp
instruction while the other does not.
It appears the cmp
instruction is an idiom is used to ensure a variable is not null; dereferencing a register with value 0 triggers an av
that is turned into a NullReferenceException
. However in StructDisptach.CallViaStruct
I cannot conceive of a way for ecx
to be null given it's pointing at a struct.
UPDATE: The answer I'm looking to accept will include code that causes a NRE to be thrown by StructDisptach.CallViaStruct
by having it's cmp
instruction dereference a zeroed ecx
register. Note this is easy to do with either of the CallViaClass
methods by setting m_class = null
and impossible to do with ClassDisptach.CallViaStruct
as there is no cmp
instruction.
using System.Runtime.CompilerServices;
namespace NativeImageTest {
struct Struct {
public void NoOp() { }
}
class Class {
public void NoOp() { }
}
class ClassDisptach {
Class m_class;
Struct m_struct;
internal ClassDisptach(Class cls) {
m_class = cls;
m_struct = new Struct();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaClass() {
m_class.NoOp();
//push ebp
//mov ebp,esp
//mov eax,dword ptr [ecx+4]
//cmp byte ptr [eax],al
//pop ebp
//ret
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
//push ebp
//mov ebp,esp
//pop ebp
//ret
}
}
struct StructDisptach {
Class m_class;
Struct m_struct;
internal StructDisptach(Class cls) {
m_class = cls;
m_struct = new Struct();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaClass() {
m_class.NoOp();
//push ebp
//mov ebp,esp
//mov eax,dword ptr [ecx]
//cmp byte ptr [eax],al
//pop ebp
//ret
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
//push ebp
//mov ebp,esp
//cmp byte ptr [ecx],al
//pop ebp
//ret
}
}
class Program {
static void Main(string[] args) {
var classDispatch = new ClassDisptach(new Class());
classDispatch.CallViaClass();
classDispatch.CallViaStruct();
var structDispatch = new StructDisptach(new Class());
structDispatch.CallViaClass();
structDispatch.CallViaStruct();
}
}
}
UPDATE: Turns out it's possible to use callvirt
on a non-virtual function which has a side effect of null checking the this pointer. While this is the case for the CallViaClass
callsite (which is why we see the null check there) StructDispatch.CallViaStruct
uses a call
instruction.
.method public hidebysig instance void CallViaClass() cil managed noinlining
{
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld class NativeImageTest.Class NativeImageTest.StructDisptach::m_class
IL_0006: callvirt instance void NativeImageTest.Class::NoOp()
IL_000b: ret
} // end of method StructDisptach::CallViaClass
.method public hidebysig instance void CallViaStruct() cil managed noinlining
{
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldflda valuetype NativeImageTest.Struct NativeImageTest.StructDisptach::m_struct
IL_0006: call instance void NativeImageTest.Struct::NoOp()
IL_000b: ret
} // end of method StructDisptach::CallViaStruct
UPDATE: There was a suggestion that the cmp
could be trapping for the case where a null
this pointer was not trapped for at the call site. If that were the case then I'd expect the the cmp
to occur once at the top of the method. However it appears once for each call to NoOp
:
struct StructDisptach {
Struct m_struct;
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
m_struct.NoOp();
//push ebp
//mov ebp,esp
//cmp byte ptr [ecx],al
//cmp byte ptr [ecx],al
//pop ebp
//ret
}
}
It was mainly developed as a system programming language to write operating system. The main features of C language include low-level access to memory, simple set of keywords, and clean style, these features make C language suitable for system programming like operating system or compiler development.
In C programming language, %d and %i are format specifiers as where %d specifies the type of variable as decimal and %i specifies the type as integer. In usage terms, there is no difference in printf() function output while printing a number using %d or %i but using scanf the difference occurs.
Role of Semicolon in C: Semicolons are end statements in C. The Semicolon tells that the current statement has been terminated and other statements following are new statements. Usage of Semicolon in C will remove ambiguity and confusion while looking at the code.
After language 'B', Dennis Ritchie came up with another language which was based upon 'B'. As in alphabets B is followed by C and hence he called this language as 'C'.
Short answer: The JITter cannot prove that the struct is not referenced by a pointer, and must at least dereference at least once on every call to NoOp() for correct behavior.
Long answer: Structs are weird.
The JITter is conservative. Wherever possible, it can only optimize the code in ways that it can be absolutely certain produce correct behavior. "Mostly-correct" isn't good enough.
So now here's an example scenario that would break if the JITter optimized away the dereference. Consider the following facts:
First: Remember that structs can (and do!) exist outside C# — a pointer to a StructDispatch could come from unmanaged code, for example. As Lucas pointed out, you can use pointers to cheat; but the JITter can't know for sure that you aren't using pointers to StructDispatch somewhere else in the code.
Second: Remember that in unmanaged code, which is the biggest reason structs exist in the first place, all bets are off. Just because you just read a value from memory doesn't mean it'll be the same value or even be a value the next time you read that same exact address. Threading, and multiprocessing, can literally have something change that value on the next clock tick, to say nothing of non-CPU actors like DMA. A parallel thread could VirtualFree() the page that contains that struct, and the JITter has to guard against that. You asked for reads from memory, so you get reads from memory. My guess is that if you kicked in the optimizer, it would remove one of those cmp instructions, but I highly doubt that it would remove both.
Third: Exceptions are real code too. NullReferenceException doesn't necessarily stop the program; it can be caught and handled. That means that from the JITter's perspective, NRE is more like an if-statement than a goto: It's a kind of condition branch that must be handled and considered on every memory dereference.
So now put those pieces together.
The JITter doesn't know — and can't know — that you're not using unsafe C# or an external source somewhere else to interact with StructDispatch's memory. It doesn't produce separate implementations of CallViaStruct(), one for "probably safe C# code" and one for "possibly risky external code;" it produces the conservative version for possibly risky scenarios, always. This means that it can't just cut out calls to NoOp() in full, because there's no guarantee that StructDispatch isn't, say, mapped to an address that isn't even paged into memory.
It knows that NoOp() is empty and can be elided (the call can go away), but it at least has to simulate the ldfla by poking the memory address of the struct, because there could be code depending on that NRE being raised. Memory dereferences are like if-statements: They can cause a branch, and failing to cause a branch may result in a broken program. Microsoft can't make assumptions and just say, "Your code shouldn't rely on that." Imagine the angry phone call to Microsoft if an NRE wasn't written to a business's error log just because the JITter decided it wasn't an "important enough" NRE to trigger in the first place. The JITter has no choice but to dereference that address at least once to ensure correct semantics.
Classes don't have any of these concerns; there's no enforced memory weirdness with a class. But structs, though, are quirkier.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With