Friday, July 07, 2006

The not so lazy garbage collector

A poster on microsoft.public.dotnet.languages.csharp (the original post is here) wrote in the other day about a bug he had - an access violation. He wasn't using any unmanaged code, so my curiosity was piqued. The reason? It's theoretically impossible to AV without using unmanaged code, so he had almost certainly found a CLR or BCL bug. I was duly motivated to get to the root of the problem.

I'm very glad I stuck with it, because I found out something I wasn't aware of, something which will make me very careful in similar scenarios in the future. If you're not interested in the path to discovery, the summary is at the end.

I did some investigation, and I discovered that the problem lay in the Ping class. Here's some code which reproduces the problem:

using System;
using System.Net;
using System.Net.NetworkInformation;

class App
{
    static void Main()
    {
        try
        {
            for (;;)
                new Ping().Send(IPAddress.Loopback);
        }
        catch (Exception ex)
        {
            if (ex.InnerException != null)
                ex = ex.InnerException;
            Console.WriteLine("Error: {0} ({1})", ex.Message,
                                                  ex.GetType().Name);
        }
    }
}
(I've registered this as a bug on the Microsoft Connect site.)

This wasn't enough for me. I wanted to know how it happened. I looked at the code for Ping using .NET Reflector, but it wasn't obvious to me. I fired up WinDbg and ran the above code until the access violation exception was triggered (it can take some seconds, depending on your machine):

(a94.87c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=04f20b74 ebx=00000000 ecx=00000008 edx=00000000 esi=04f20b54 edi=012e7580
eip=78144d3a esp=0012f278 ebp=0012f280 iopl=0         nv up ei pl zr na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
MSVCR80!memcpy+0x5a:
78144d3a f3a5            rep  movsd ds:04f20b54=???????? es:012e7580=00000000
So, it was failing in memcpy. Where was that called from? SOS and !dumpstack gives the right info (excerpted):
> .load sos
> !dumpstack

OS Thread Id: 0x87c (0)
Current frame: MSVCR80!memcpy+0x5a [F:\RTM\vctools\...\intel\memcpy.asm:188]
ChildEBP RetAddr  Caller,Callee
0012f280 79e808d5 mscorwks!memcpyNoGCRefs+0x11, // ...
0012f290 79f02572 mscorwks!MarshalNative::CopyToManaged+0x11b, // ...
0012f2e0 79f02491 mscorwks!MarshalNative::CopyToManaged+0x22, // ...
0012f340 7a615416 (/*...*/ System.Net.NetworkInformation.PingReply..ctor(// ...
0012f358 7a61474e (/*...*/ System.Net.NetworkInformation.Ping.InternalSend(// ...
0012f37c 79eec356 mscorwks!FastAllocateObject+0xa6, calling mscorwks!Object// ...
0012f3fc 7a6139b4 (/*...*/ System.Net.NetworkInformation.Ping.Send(System.// ...
0012f43c 7a613752 (/*...*/ System.Net.NetworkInformation.Ping.Send(System.// ...
0012f454 00ca009e (MethodDesc 0x922fd8 +0x2e App.Main()), calling 7a87f55c
// ...
I can see here that the last managed code was inside PingReply's constructor. Here's what the appropriate constructor overload looks like, roughly:
internal PingReply(IcmpEchoReply reply)
{
    this.address = new IPAddress((long) reply.address);
    this.ipStatus = (IPStatus) reply.status;
    if (this.ipStatus == IPStatus.Success)
    {
        this.rtt = reply.roundTripTime;
        this.buffer = new byte[reply.dataSize];
        Marshal.Copy(reply.data, this.buffer, 0, reply.dataSize);
        this.options = new PingOptions(reply.options);
    }
    else
    {
        this.buffer = new byte[0];
    }
}
The call to MarshalNative::CopyToManaged() is inside Marshal.Copy(), and it's copying data from reply.data (an IntPtr) to the managed array, this.buffer. This structure is filled by the Windows API function IcmpSendEcho(). The buffer is allocated by the caller, as is usual for Windows API functions. The buffer is owned through a SafeHandle descendant, SafeLocalFree, and it's owned by the Ping class.

The memcpy function compiles to an x86 string operation, with esi and edi being the source and destination registers. From the original exception data at the top, one can see that ESI (the source) is an invalid pointer - that's what caused the access violation. Thus, the buffer has been freed early.

How did that happen? One can use !dso (aka !dumpstackobjects) to find all the live objects rooted on the stack or registers:

> !dso

OS Thread Id: 0x87c (0)
ESP/REG  Object   Name
0012f2ac 012e7508 System.Net.NetworkInformation.PingReply
0012f2c0 012e7508 System.Net.NetworkInformation.PingReply
0012f32c 012e7578 System.Byte[]
0012f350 012e7508 System.Net.NetworkInformation.PingReply
0012f410 012e74b4 System.Byte[]
0012f438 0128142c System.Net.IPAddress
0012f448 012e74b4 System.Byte[]
Aha! There's no Ping object! Even though several instance methods of the Ping object (two Ping.Send overloads and Ping.InternalSend) are currently executing, Ping is in fact eligible for garbage collection!

It appears that's what's happened. Let's see if that can be confirmed. If one runs !dumpheap -stat, there's still lots of Pings on the heap:

> !dumpheap -stat

// ...
7a779154     2041        40820 System.Net.SafeLocalFree
7a778ec0     2041        40820 System.Net.SafeCloseHandle
7915ff38     2041        65312 System.Threading.SendOrPostCallback
79124418     2043        89864 System.Byte[]
7a7812e0     2041       179608 System.Net.NetworkInformation.Ping
// ...
Let's see if we can find the Ping that's on our call stack:
> !dumpheap -type Ping

// ...
012e7040 7a7812e0       88     
012e710c 7a7812e0       88     
012e71d8 7a7812e0       88     
012e72a4 7a7812e0       88     
012e7370 7a7812e0       88     
012e743c 7a7812e0       88     
012e7508 7a781580       32     
total 2042 objects
Statistics:
      MT    Count    TotalSize Class Name
7a781580        1           32 System.Net.NetworkInformation.PingReply
7a7812e0     2041       179608 System.Net.NetworkInformation.Ping
Total 2042 objects
Now, since there is only one thread allocating Pings and there's nothing keeping them alive, I can expect that the Ping in the highest address is the most recently allocated one. To confirm that, I'll test the last two Ping objects listed (012e743c and 012e7370):
> !gcroot 012e7370
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 87c
Scan Thread 2 OSTHread 828

> !gcroot 012e743c
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 87c
ESP:12f390:Root:0130e1cc(System.Net.NetworkInformation.Ping)->
012e7494(System.Threading.SendOrPostCallback)->
012e743c(System.Net.NetworkInformation.Ping)
ESP:12f3f8:Root:0130e1cc(System.Net.NetworkInformation.Ping)->
012e7494(System.Threading.SendOrPostCallback)
ESP:12f434:Root:0130e1cc(System.Net.NetworkInformation.Ping)->
012e7494(System.Threading.SendOrPostCallback)
ESP:12f450:Root:0130e1cc(System.Net.NetworkInformation.Ping)->
012e7494(System.Threading.SendOrPostCallback)
Scan Thread 2 OSTHread 828
Here it can be seen that of the two, only 012e743c is alive. Because of the pattern of the loop, it would be expected that any locations holding Ping would be overwritten each time around, so this Ping at 012e743c should be the right one. Peeking inside it with !do (aka !dumpobject):
> !do 012e743c

Name: System.Net.NetworkInformation.Ping
MethodTable: 7a7812e0
EEClass: 7a7e9100
Size: 88(0x58) bytes
 (C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
790f9c18  4000184        4        System.Object  0 instance 00000000 __identity
// ...
7a779154  4002c06       20 ...Net.SafeLocalFree  0 instance 012e74f4 replyBuffer
// ...
I'm really only interested in this guy, the replyBuffer - because that's the guy who's pointing to invalid memory. Drilling into that location:
> !do 012e74f4

Name: System.Net.SafeLocalFree
MethodTable: 7a779154
EEClass: 7a7db890
Size: 20(0x14) bytes
 (C:\WINDOWS\assembly\GAC_MSIL\System\2.0.0.0__b77a5c561934e089\System.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
790fe160  40005b4        4        System.IntPtr  0 instance 82971448 handle
790fed1c  40005b5        8         System.Int32  0 instance        3 _state
79104f64  40005b6        c       System.Boolean  0 instance        1 _ownsHandle
79104f64  40005b7        d       System.Boolean  0 instance        1 _fullyInitialized
7a779154  40025ec      948 ...Net.SafeLocalFree  0   static 00000000 Zero
Now, the layout of the _state field is explained in the SSCLI 2.0, in the file clr/src/vm/safehandle.cpp:
// So the state field ends up looking like this:
//
//  31                                                        2  1   0
// +-----------------------------------------------------------+---+---+
// |                           Ref count                       | D | C |
// +-----------------------------------------------------------+---+---+
// 
// Where D = 1 means a Dispose has been performed and C = 1 means the
// underlying handle has (or will be shortly) released.
This shows that a _state value of 3 means that it's been both disposed and released. So, even though the Ping and SafeLocalFree objects haven't actually been garbage collected, the eager finalization of SafeHandle objects has released the buffer early. I'll consider that definitive.

Summary

There's at least two lessons from this:
  1. In general, one cannot rely on the fact that an instance method is on the call stack to keep the instance alive. One needn't be too paranoid, though: the objects can only be collected if the current line in each instance method is near or at the end of the method, so that 'this' is no longer on the stack or enregistered.
  2. Thus, one needs to be extra careful to keep SafeHandle instances alive, either by passing them along with any buffer / handle value extracted, or by using SafeHandle.DangerousAddRef()/SafeHandle.DangerousRelease() in a try/finally block to be absolutely sure that an early release doesn't occur.
Finally, to make everything crystal clear - what will the following program print?
using System;

class App
{
    class A
    {
        public B Foo()
        {
            return new B();
        }
        
        ~A()
        {
            Console.WriteLine("A finalized.");
        }
    }
    
    class B
    {
        public B()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("B now returning.");
        }
    }
    
    static void Main()
    {
        new A().Foo();
    }
}
If you've followed everything, you'll know that this is what it prints (compiled in Release mode):
A finalized.
B now returning.
Maybe this was obvious to everybody - but it certainly wasn't obvious to me, nor to the designer of the Ping class!

Tuesday, July 04, 2006

Covariance and Contravariance in .NET, Java and C++

Prompted by a Microsoft Research paper I read recently and a post on the MS .NET newsgroups, I investigated covariance and contravariance support in .NET, and contrasted it with the support in Java.

I won't describe try to describe covariance or contravariance directly. Hopefully it'll be clear from the examples. A brief description of covariance and contravariance can be found here (Wikipedia).

To begin with, I'll enumerate some of the different kinds of variance supported in C#, Common Language Infrastructure (CLI), C++ and Java, as far as I know them, along with the definitions I'll use in this article. These definitions aren't official in any sense I'm aware of.

  1. Override Variance

    This variance refers to the parameters and return types of an overridden method in a descendant class. C++ supports override covariance of return types.

  2. Definition-site Generic Variance

    With this kind of variance, the generic type, as part of its definition, defines how the subtype relation applies to instantiations of the generic type when the type arguments are themselves related by the subtype relation. The CLI (and thus the CLR) supports definition-site generic variance.

  3. Use-site Generic Variance

    With this kind of variance, the generic variable declaration (i.e. parameter, local or field), as part of the declaration, defines whether or not it is assignment-compatible with generic instantiations whose parameter types are more derived (covariant) or less derived (contravariant). Java wildcards are an implementation of use-site generic variance.

  4. Array covariance

    Java supports covariance of arrays of object types. This covariance isn't fully sound with respect to the type system at compile time, because arrays are mutable. Thus, run-time checks are used to patch up the hole. C# and the CLI support this feature chiefly to support Java on the CLI. To demonstrate the hole:

    Dog[] dogs = new Dog[10];
    Mammal[] mammals = dogs;
    mammals[0] = new Cat();
    
    The above code is statically correct with respect to types at compile time, with the Java definition of array covariance, but of course it isn't actually statically type-safe.
  5. Delegate variance

    C# supports delegate variance only at the point of binding. The method to which a delegate value is bound may have a covariant return type and contravariant argument types. Once the delegate is bound, it is not assignment-compatible with another delegate type even where the underlying method would be compatible according to variance rules.

I'll drill a bit deeper into the first three of these, since most developers should be familiar with the last two.

Override Variance

This variance refers to the parameters and return types of an overridden method in a descendant class. C++ supports this for covariance of return types. Return types and out parameters may be covariant, input parameters may be contravariant, and in-out parameters must be invariant. C++ example:
class Mammal
{
public:
    virtual Mammal* GetValue();
};

class Dog : public Mammal
{
public:
    virtual Dog* GetValue();
};
The equivalent example to demonstrate contravariance of input parameters can't be written in C++, since C++ doesn't support it. If one could declare it, it would look a bit like the following C#:
class DogComparer
{
    public virtual int Compare(Dog left, Dog right)
    {
    }
}

class MammalComparer : DogComparer
{
    public override int Compare(Mammal left, Mammal right)
    {
    }
}
Note the difference: the inheritance relationship is the other way around. That's where the 'contra' comes in. The arguments' subtype relationship is the opposite of the outer type's subtype relationship.

It's also intuitively true. A comparer of mammals is naturally also a comparer of dogs, since dogs are a subtype of mammal - thus a comparer of mammals is a subtype of a comparer of dogs!

Neither C#, the CLI nor Java support override variance. C++ supports return type override covariance, but not override contravariant input arguments. C++/CLI doesn't support override covariant return types for managed classes. It gives this error:

error C2392: 'Dog ^Dog::GetValue(void)' : covariant returns types are
not supported in managed types, otherwise 'Mammal ^Mammal::GetValue(void)' would be overridden

Definition-site Generic Variance

The CLI (II 9.5) supports definition-site variance for interfaces and delegates, but not for reference classes and value types. It uses the syntax <+T> to denote covariance and <-T> for contravariance. Because covariance only works for output parameters, a generic type can specify covariance on type parameters which are used in output positions only. Similiarly, contravariance is allowed on type parameters which are used for input only.

Within these constraints, and pretending that C# supported this CLI feature, we could envision these types:

class Mammal { }
class Dog : Mammal { }

interface IReader<+T> // allows covariance
{
    T GetValue();
}

interface IWriter<-T> // allows contravariance
{
    void SetValue(T value);
}
With these definitions, covariance of generic parameters would allow this:
  IReader<Dog> dogReader = null;
  IReader<Mammal> mammalReader = dogReader;
Contravariance of generic parameters would allow this:
  IWriter<Mammal> mammalWriter = null;
  IWriter<Dog> dogWriter = mammalWriter;
These are both disallowed in C#, but allowed at the IL level. Here's an IL translation of the above imaginary C# which assembles and passes PEVerify:
.assembly extern mscorlib {}
.assembly Test {}

.class private auto ansi beforefieldinit Mammal
       extends [mscorlib]System.Object {}

.class private auto ansi beforefieldinit Dog
       extends Mammal {}

.class interface private abstract auto ansi IReader`1<+T>
{
  .method public hidebysig newslot abstract virtual 
          instance !T  GetValue() cil managed {}
}

.class interface private abstract auto ansi IWriter`1<-T>
{
  .method public hidebysig newslot abstract virtual 
          instance void  SetValue(!T 'value') cil managed {}
}

.class private auto ansi beforefieldinit App
       extends [mscorlib]System.Object
{
  .method private hidebysig static void Main() cil managed
  {
    .entrypoint
    .locals init (
             [0] class IReader`1<class dog> dogReader,
             [1] class IReader`1<class Mammal> mammalReader,
             [2] class IWriter`1<class Mammal> mammalWriter,
             [3] class IWriter`1<class Dog> dogWriter)
    
    ldnull
    stloc.0

    ldloc.0
    stloc.1

    ldnull
    stloc.2

    ldloc.2
    stloc.3

    ret
  }
}
If one switches around the assignments, to try and treat covariance contravariantly and vice versa, changing the body of the Main method to:
    ldnull
    stloc.1
    
    ldloc.1
    stloc.0
    
    ldnull
    stloc.3
    
    ldloc.3
    stloc.2
    
    ret
One then gets the following errors from PEVerify:
[IL]: Error: [App::Main][found ref 'IReader`1[Mammal]'][expected ref
'IReader`1[Dog]'] Unexpected type on the stack.
[IL]: Error: [App::Main][found ref 'IWriter`1[Dog]'][expected ref
'IWriter`1[Mammal]'] Unexpected type on the stack.
Similarly, if one tries to make IReader<+T> contravariant, i.e. change it to IReader<-T>, and similarly make IWriter<-T> covariant, one gets the following errors from PEVerify:
[token  0x02000004] Type load failed.
[token  0x02000005] Type load failed.
So, the covariant and contravariant support is there.

Use-site Generic Variance

This is the definition of variance at the use site, rather than the definition site. That means that when declaring variables of a generic type, one can make the variable declaration open to instances of generic types with more (covariant) or less (contravariant) derived type arguments.

To make the example concrete, I'll use Java 5, which supports covariance and contravariance through wildcards.

class Mammal {
    public Mammal() {
    }
}

// ---

class Dog extends Mammal {
    public Dog() {
    }
}

// ---

class Cat extends Mammal {
    public Cat() {
    }
}

// ---

public class Holder<T> {
    T _value;
    
    public Holder() {
    }
    
    public T getValue() {
        return _value;
    }
    
    public void setValue(T value) {
        _value = value;
    }
}
Given these definitions, I can make use of covariance thusly:
Holder<Cat> catHolder = new Holder<Cat>();
catHolder.setValue(new Cat());
// Use covariance to fit cat-holder into mammal-holder.
Holder<? extends Mammal> mammalHolder = catHolder;
// Can now access return values (covariant is only safe for out).
Mammal mammal = mammalHolder.getValue();
System.out.println(mammal);
// This won't work: covariance doesn't work for input parameters.
mammalHolder.setValue(new Dog());
Similarly, I can make use of contravariance:
Holder<Mammal> mammalHolder = new Holder<Mammal>();
// Use contravariance to fit mammal-holder into cat-holder.
Holder<? super Cat> catHolder = mammalHolder;
// Can now access input parameters (contravariance is only
// safe for input).
catHolder.setValue(new Cat());
// This won't work: contravariance doesn't allow output.
Cat cat = catHolder.getValue();

Summary

The CLI and Java have two quite different generic variance capabilities. C# hasn't exposed any of the CLI's generic variance. The delegate variance exposed by C# appears to be more a feature of the CLR / CLI's loosening of delegate binding restrictions, since it doesn't use generic variance functionality. Intuitively, it appears that use-site generic variance is a superset of the functionality of definition-site variance, since any definition-site variance scheme can be replaced by an equivalent use-site version, while the contrary isn't true (input arguments of covariant generic types and output arguments of contravariant generic types are strictly disallowed in definitions, but may be disallowed on a case-by-case basis at the use-point) - but I haven't tried to prove that.

There is, however, a cost associated with generic type variance - conceptual complexity. Covariance and contravariance are simple enough once one gets used to them, but they represent yet another barrier to be overcome for newcomers to a language.

Monday, May 29, 2006

Call vs CallVirt for C# non-virtual instance methods

Prompted by a post over in micrsoft.public.dotnet.languages.csharp, where someone asked if the C# compiler should issue a warning for an expression like:
this == null
The answer is no, for a fairly complicated reason. If C# was the only compiler for the CLR, then the poster might have a point - calling an instance method on a null instance always throws an exception in C#. However, other languages targeting the CLR can invoke a non-virtual instance method on a null instance, without error. In particular, the Delphi object model's TObject.Free method takes advantage of this to only call the destructor for non-null objects. How does this work under the covers? Well, it comes down to the difference in semantics between the 'call' and 'callvirt' CIL instructions. Note: everything I mention in this entry applies only to non-virtual instance methods.

One time I am aware of that the C# compiler generates a 'call' instruction is when calling the base class's method for overridden virtual methods. In that case, the compiler can use 'call' since the instance can't be null because the method was (ultimately) called using virtual dynamic dispatch.

The rationale for using 'callvirt' instead of 'call' for C# non-virtual instance methods is, I would guess, to fail sooner. When calling an instance method on a null instance, the null instance is found sooner than it might be. For example, a method might check its arguments only and thereby determine that nothing needs doing in this case, and return without causing an exception. If the compiler didn't generate code that checked the instance, the fact that an instance method was called on a null reference might not be caught until later.

What's the difference in JIT-compiled code between 'call' and 'callvirt' on CLR 2.0.50727?

For this analysis, I started with this CIL:

.assembly extern mscorlib {}
.assembly Test {}
.subsystem 0x0003

.class App extends [mscorlib]System.Object
{
    .method public instance void Test()
    {
        ldstr "This is null\? {0}"
        
        ldarg.0
        ldnull
        ceq
        box [mscorlib]System.Boolean
        call void [mscorlib]System.Console::WriteLine(string,object)
        ret
    }
    
    .method public static void Main()
    {
        .entrypoint
        
        ldstr "First:"
        call void [mscorlib]System.Console::WriteLine(string)
        ldnull
        call instance void App::Test()
        
        ldstr "Second:"
        call void [mscorlib]System.Console::WriteLine(string)
        ldnull
        callvirt instance void App::Test()
        ret
    }
}
Roughly transliterated into C# code, it looks like this:
using System;

class App
{
    public void Test()
    {
        Console.WriteLine("This is null? {0}", this == null);
    }

    public static void Main()
    {
        Console.WriteLine("First:");
        ((App) null).Test(); // with 'call': not possible in MS C# 2.0
        Console.WriteLine("Second:");
        ((App) null).Test(); // with 'callvirt': default for C# compiler
    }
}
This assembly's name is Test, but I compiled it to an executable called CallVirt.exe, with ilasm, and started the VS 2005 debugger:
ilasm -debug=opt CallVirt.il
devenv -debugexe CallVirt.exe
I changed the project's debugger settings from Auto to Mixed, and stepped into the code. When disassembled with SOS, the code for the App.Main method looks like this:
.load sos
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

!name2ee CallVirt.exe App.Main
PDB symbol for mscorwks.dll not loaded
Module: 00912c14 (CallVirt.exe)
Token: 0x06000002
MethodDesc: 00912fe0
Name: App.Main()
JITTED Code Address: 00de0070

!u 00de0070
Normal JIT generated code
App.Main()
Begin 00de0070, size 61
>>> 00DE0070 833D84102B0200   cmp         dword ptr ds:[022B1084h],0
00DE0077 750A             jne         00DE0083
00DE0079 B901000000       mov         ecx,1
00DE007E E889D75678       call        7934D80C 
         (System.Console.InitializeStdOutError(Boolean), mdToken: 0600070f)
00DE0083 8B0D84102B02     mov         ecx,dword ptr ds:[022B1084h]
00DE0089 8B153C302B02     mov         edx,dword ptr ds:[022B303Ch]
00DE008F 8B01             mov         eax,dword ptr [ecx]
00DE0091 FF90D8000000     call        dword ptr [eax+000000D8h]
00DE0097 33C9             xor         ecx,ecx
00DE0099 FF1520309100     call        dword ptr ds:[00913020h]
00DE009F 833D84102B0200   cmp         dword ptr ds:[022B1084h],0
00DE00A6 750A             jne         00DE00B2
00DE00A8 B901000000       mov         ecx,1
00DE00AD E85AD75678       call        7934D80C 
         (System.Console.InitializeStdOutError(Boolean), mdToken: 0600070f)
00DE00B2 8B0D84102B02     mov         ecx,dword ptr ds:[022B1084h]
00DE00B8 8B1540302B02     mov         edx,dword ptr ds:[022B3040h]
00DE00BE 8B01             mov         eax,dword ptr [ecx]
00DE00C0 FF90D8000000     call        dword ptr [eax+000000D8h]
00DE00C6 33C9             xor         ecx,ecx
00DE00C8 3909             cmp         dword ptr [ecx],ecx
00DE00CA FF1520309100     call        dword ptr ds:[00913020h]
00DE00D0 C3               ret
This code is longer that strictly "necessary" because the Console::WriteLine(string) method has been inlined. The two relevant snippets of code for 'call' and 'callvirt', including the setting of the 'this' argument to null, are as follows:
// CALL
00DE0097 33C9             xor         ecx,ecx
00DE0099 FF1520309100     call        dword ptr ds:[00913020h]

// CALLVIRT
00DE00C6 33C9             xor         ecx,ecx
00DE00C8 3909             cmp         dword ptr [ecx],ecx
00DE00CA FF1520309100     call        dword ptr ds:[00913020h]
Thus, the difference with CALLVIRT is that it tests the pointer by dereferencing it. That causes a hardware exception when the pointer is null, and that hardware exception gets propagated to the CLR via Windows SEH.

Something interesting that can be observed from this: the calls to the App.Test() method are through an indirection. A peek in the address shows the data:

>d -format:fourbytes 0x00913020
0x00913020  00de00e8 00de0070 00000080 022b1ec4  
0x00913030  912fd8b8 e9ed8900 ffa2eed0 912fe0b8  
0x00913040  e9ed8900 ffa2eec4 576f62e8 cccc5e79  
0x00913050  00912fe0 00000000 00000000 00000000  
One can then disassemble the code at the indirect location:
!u 0x00de00e8

Normal JIT generated code
App.Test()
Begin 00de00e8, size 44
>>> 00DE00E8 57               push        edi

... etc.
So, calls to non-virtual instance methods compiled with the current C# compiler get turned into CIL 'callvirt' instructions, which, with the current JIT compiler, test the 'this' argument with a CMP instruction. Other languages which use 'call' simply call straight through without the test.

Sunday, May 21, 2006

Delphi/Win32 and COM Interface casting

Over in borland.public.delphi.language.delphi.win32, Kevin Donn asked some questions about memory allocations caused by casting Delphi object instances to interfaces (which in Delphi/Win32, are always COM interfaces). My answer applies to Delphi/Win32 only.
Presumably this would not create a memory leak:
var
  i: IMyInterface
  o: TMyObject // supports IMyInterface
begin
  i:=o as IMyInterface
end
The opposite may be true: it might free the object sooner than you think. Interfaces in Delphi/Win32 on classes that derive from TInterfacedObject follow COM rules. That basically means that mixing object references and interface references is dodgy. As soon as you cast or assign an object reference to an interface reference, it gets AddRef'd for the first time. When the last interface reference goes out of scope, it gets Release'd. If you still have an object reference to the object, then it will be a bad pointer - nasty.

It's best to either stick to COM rules and only access the object through interfaces (and thus get refcounted lifetime management), or else implement IInterface (aka IUnknown) yourself and use manual memory management.

But, wisdom aside, will the following cause a memory leak?
var
  p: pointer
  o: TMyObject // supports IMyInterface
begin
  p:=pointer(o as IMyInterface)
end
This will create a temporary value (i.e. kind of an anonymous local variable) of type IMyInterface (which gets AddRef'd during this process), convert the interface address to a pointer, then Release's the interface. That may or may not free TMyObject. If it freed TMyObject, then both o and p will point to dead memory. If it didn't, then p is still valid, but it's just a pointer into o's memory. No memory is allocated in this process, but it might be freed, if there were no interfaces pointing to o in scope.

Pointers to interfaces are pointers into the middle of the object. A picture:

--- TMyObject ---
0: TMyObject metaclass pointer --->
// ...
n: TMyObject's IMyInterface vtable --->
// ...
// object data
-----------------

--- TMyObject.IMyInterface vtable ---
@QueryInterface_adjust_self_and_jump
@_AddRef_adjust_self_and_jump
@_Release_adjust_self_and_jump
// ... other IMyInterface methods
-------------------------------------
Normally, a pointer to a value of type TMyObject points to the start of the object, which itself points to the metaclass (i.e. TMyObject).

A pointer to an interface points to a vtable. This is defined by COM, which is a binary standard. This vtable is a list of function pointers. (These functions adjust the 'Self' pointer that is passed in as the first argument, and then jump to the real implementation of the methods.)

I'd draw better pictures, but it's very tedious in ASCII.

Both TMyObject and the vtables are statically allocated as part of the EXE or DLL image, and don't need to be freed.

Perhaps this program may make things clearer:

program Test;

uses SysUtils, Classes;

{$APPTYPE CONSOLE}

procedure Dump(Start: Pointer; Count: Integer);
var
  p: PPointer;
begin
  p := Start;
  while Count > 0 do
  begin
    Dec(Count);
    Writeln(Format('%p: %p', [p, p^]));
    Inc(p);
  end;
end;

var
  o: TInterfacedObject;
  i: IInterface;
begin
  o := TInterfacedObject.Create;
  Writeln('The Object');
  Dump(o, 4);
  Writeln('The Class');
  Dump(TInterfacedObject, 4);
  i := o;
  Writeln('The Interface');
  Dump(Pointer(i), 4);
end.
When I run it on my system, this is what I get:
The Object
00A14E60: 0040111C
00A14E64: 00000000
00A14E68: 004010A1
00A14E6C: 00A14E81
The Class
0040111C: 6E495411
00401120: 66726574
00401124: 64656361
00401128: 656A624F
The Interface
00A14E68: 004010A1
00A14E6C: 00A14E81
00A14E70: 00000000
00A14E74: 00000001
Notice that the interface pointer is (in this case) at an offset of 8 from the object pointer. You can see that the vtable for TInterfacedObject's IInterface implementation is at $4010A1, while the metaclass is located at $40111C - relatively close together. Since .EXE images in Windows get linked so that their load address starts at $400000, you can infer from this that the metaclass and interface vtable are both part of the .EXE image.
More specifically, does the generation of an interface cause memory allocation and if so how does it get cleaned up?
The only memory used is part of the object, unless you've delegated the interface implementation to a property which returns an object derived from TAggregatedObject - which itself delegates AddRef and Release to its controller, the parent object.

I hope this makes it clearer. It's not a totally trivial question. You need to know what's going on beneath the hood to understand and use (and most especially implement) COM interfaces with any level of sophistication.

Wednesday, May 17, 2006

CLR TailCall Optimization (or lack thereof)

I've neglected this blog lately because I didn't know what I'd be putting in it. I'm not the type of person to post a public diary of my daily minutia, yet dry didactic posts like my first bits on compiler implementation don't excite me either. So, the new approach I'm going to take is to put up on here some of my more interesting problems, solutions and analyses. That way they'll be there for my own reference later, I'll be able to point to them when answering questions on newsgroups, and perhaps even the good burghers of the net may wander in via Google.

Tasos Vogiatzoglou asked a question on the microsoft.public.dotnet.framework.clr newsgroup, wondering why tail. call was a slow MSIL sequence on the current .NET 2.0 CLR. My reply, including analysis, is below.

I assume that if the tail. command is supported by the jit (I do not think that is supported) it will be used in rather rare conditions of fully trusted code and perhaps code that does not access the execution stack (via stacktrace or sth) .

I think it is not optimized because no mainstream language currently uses it. It certainly is implemented in so far as the stack does not grow when you use tail. call to jump to the start of a method.

514 ms (with tailcall) / 77 ms (without tailcall). I cannot understand this ... Can anyone provide any helpful insight ?
The fact is, tail. call is not optimized via JIT to a jump currently. And when you try to debug all this under VS 2005, it lies to you about the code!

I started with this:

using System;

class App
{
    static double ArithmeticSum(int number, double result)
    {
        if (number == 0)
            return result;
        return ArithmeticSum(number - 1, number + result);
    }
    
    static void Main()
    {
        double result = 0;
        for (int i = 0; i < 10000; ++i)
            result = ArithmeticSum(10000, 1);
        Console.WriteLine(result);
    }
}
I disassembled with ildasm, and rewrote ArithmeticSum to use tail. call:
// ...
    IL_000c:  ldarg.1
    IL_000d:  stloc.0
    IL_000e:  br.s       exit
// ...
    IL_0015:  ldarg.1
    IL_0016:  add
    
    tail. call float64 App::ArithmeticSum(int32, float64)
    ret
    
exit:
 ldloc.0
 ret
  } // end of method App::ArithmeticSum
I reassembled with ilasm /debug=OPT and, like you, I found that the tail-call version was much slower. So, I started "devenv /debugexe Test.exe", and stepped into the code.

This is what VS says about the JIT compiled code in the disassembly window:

    IL_0000:  nop
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  
00000006  push        eax  
00000007  fld         qword ptr [ebp+8] 
    IL_0001:  ldarg.0
0000000a  test        ecx,ecx 
0000000c  setne       al   
0000000f  movzx       eax,al 
    IL_0009:  ldloc.1
00000012  test        eax,eax 
00000014  jne         00000018 

    IL_000c:  ldarg.1
00000016  jmp         00000039 

    IL_0010:  ldarg.0
00000018  mov         dword ptr [ebp-10h],ecx 
0000001b  fild        dword ptr [ebp-10h] 
0000001e  faddp       st(1),st 
00000020  sub         esp,8 
00000023  fstp        qword ptr [esp] 
00000026  dec         ecx  
00000027  mov         eax,dword ptr ds:[00923028h] 
0000002d  push        2    
0000002f  push        2    
00000031  push        1    
00000033  push        eax  
00000034  call        791B69B0         // <------- NOTE
00000039  pop         ecx  
    
exit:
 ldloc.0
0000003a  pop         ebx  
0000003b  pop         esi  
0000003c  pop         edi  
0000003d  pop         ebp  
0000003e  ret         8    
The important bit to note is the call to 791B69B0. Even in VS 2005 mixed mode debugging, it won't let you step into this code. When you try to step into it, the instruction pointer jumps back to the start of the method - effectively the call is implementing the tail call, but VS is "helpfully" hiding the details.

(Vance Morrison at MSFT shares my annoyance with this "feature".)

So, its time to crack open SOS: In immediate window:

.load sos
!u 791B69B0
And I got this:
Unmanaged code
791B69B0 F8               clc
791B69B1 ??               db          ffh
791B69B2 ??               db          ffh
791B69B3 FF0400           inc         dword ptr [eax+eax]
791B69B6 0000             add         byte ptr [eax],al
791B69B8 0100             add         dword ptr [eax],eax
791B69BA 0000             add         byte ptr [eax],al
791B69BC 0000             add         byte ptr [eax],al
791B69BE 0C02             or          al,2
791B69C0 1000             adc         byte ptr [eax],al
There's something fishy going on here: this code isn't meaningfully executable!

So, I looked up my dear friend App.ArithmeticSum:

!name2ee Test.exe App.ArithmeticSum
Module: 00922c14 (Test.exe)
Token: 0x06000001
MethodDesc: 00922fd8
Name: App.ArithmeticSum(Int32, Double)
JITTED Code Address: 00de0100

!u 00de0100
Normal JIT generated code
App.ArithmeticSum(Int32, Double)
Begin 00de0100, size 41
>>> 00DE0100 55               push        ebp
00DE0101 8BEC             mov         ebp,esp
00DE0103 57               push        edi
00DE0104 56               push        esi
00DE0105 53               push        ebx
00DE0106 50               push        eax
00DE0107 DD4508           fld         qword ptr [ebp+8]
00DE010A 85C9             test        ecx,ecx
00DE010C 0F95C0           setne       al
00DE010F 0FB6C0           movzx       eax,al
00DE0112 85C0             test        eax,eax
00DE0114 7502             jne         00DE0118
00DE0116 EB21             jmp         00DE0139
00DE0118 894DF0           mov         dword ptr [ebp-10h],ecx
00DE011B DB45F0           fild        dword ptr [ebp-10h]
00DE011E DEC1             faddp       st(1),st
00DE0120 83EC08           sub         esp,8
00DE0123 DD1C24           fstp        qword ptr [esp]
00DE0126 49               dec         ecx
00DE0127 8B0528309200     mov         eax,dword ptr ds:[00923028h]
00DE012D 6A02             push        2
00DE012F 6A02             push        2
00DE0131 6A01             push        1
00DE0133 50               push        eax
00DE0134 E877691B79       call        79F96AB0 (JitHelp:
CORINFO_HELP_TAILCALL)
00DE0139 59               pop         ecx
00DE013A 5B               pop         ebx
00DE013B 5E               pop         esi
00DE013C 5F               pop         edi
00DE013D 5D               pop         ebp
00DE013E C20800           ret         8
Here, note the LIE THAT VISUAL STUDIO TOLD!
    call        79F96AB0 (JitHelp: CORINFO_HELP_TAILCALL)
This address, 79F96AB0, is different from the one in VS's most excellent diassembly view, 791B69B0.

A peek inside this method (which, as we can see from timings, must be quite expensive and is thus probably pretty complex):

!u 79F96AB0
Unmanaged code
79F96AB0 FF155012387A     call        dword ptr ds:[7A381250h]
79F96AB6 50               push        eax
79F96AB7 51               push        ecx
79F96AB8 52               push        edx
79F96AB9 F6058C44397AFF   test        byte ptr ds:[7A39448Ch],0FFh
79F96AC0 7409             je          79F96ACB
79F96AC2 F740045F000000   test        dword ptr [eax+4],5Fh
79F96AC9 7422             je          79F96AED
79F96ACB 68DDDDDDDD       push        0DDDDDDDDh
79F96AD0 68CCCCCCCC       push        0CCCCCCCCh
Wonder what this is doing? A quick grep through the SSCLI 2.0 sources gives this line:
./inc/jithelpers.h:    JITHELPER(CORINFO_HELP_TAILCALL, JIT_TailCall)
Another grep for this JIT_TailCall gives this:
./vm/i386/jithelp.asm:PUBLIC JIT_TailCall
A peek inside this file gives this information (excerpted):
        call    _GetThread  ; eax = Thread*
        push    eax         ; Thread*

        ; save ArgumentRegisters
        push    ecx
        push    edx

ExtraSpace      = 12    ; pThread, ecx, edx

        ; For GC stress, we always need to trip for GC
        test    _g_TailCanSkipTripForGC, 0FFh
        jz      TripForGC
        
        ; Trip for GC only if necessary
        test    dword ptr [eax+Thread_m_State], TS_CatchAtSafePoint_ASM
        jz      NoTripForGC

TripForGC:

; Create a MachState struct on the stack

; return address is already on the stack, but is separated from stack 
; arguments by the extra arguments of JIT_TailCall. So we cant use it directly

        push    0DDDDDDDDh

; Esp on unwind. Not needed as we it is deduced from the target method

        push    0CCCCCCCCh
This looks like an exact match for the disassembled code above, especially given the magic numbers pushed.

So, if one is looking for reasons why tail. call is slow, one must look inside clr/src/vm/i386/jithelp.asm to see the work it is doing.

Here are some reasons:

  • The JIT doesn't attempt in any way to optimize for the tail. call case, so it doesn't generate machine code which would be compatible with a simple JMP.
  • The JIT_TailCall must work for all possible calls, in the presence of exception propagation, and be tolerent of possible GCs.
The best hope for getting Microsoft to optimize this is for people to complain that an important piece of software (i.e. language) that relies on tail. call runs too slowly on the CLR.

A good reason why most compilers don't normally produce tail. call for .NET already is that it removes a frame from the stack, and that interferes with code access security.