greenoldman
greenoldman

Reputation: 21062

What is the implementation difference between static variable and static field?

This question is from compiler implementation perspective.

I wondered about static variables in C# and I found the explanation why they are not implemented (here: http://blogs.msdn.com/b/csharpfaq/archive/2004/05/11/why-doesn-t-c-support-static-method-variables.aspx ).

Quote "it is possible to get nearly the same effect by having a class-level static" -- and this made me curious, what is difference? Let's say C# would have static variable syntax -- the implementation could be "push this silently as static field and leave conditional initialization (if necessary)". Done.

The only thing I can spot is the problem with value type with given initialization. Is there anything else that fits into "nearly"?

I rephrase the question -- how to implement static variables in C# compilers using only existing features (so static variable has to be internally made in current state terms).

Upvotes: 3

Views: 438

Answers (2)

Steve Cooper
Steve Cooper

Reputation: 21480

My thoughts are that you'll need to start putting 'invisible' locks on the initialiser.

Consider the case where two threads simultaneously class Foo.UseStatic;

class Foo
{
    static int counter = 0;

    void UsesStatic()
    {
        static int bar = (counter++) + (counter++);
    }
}

The initialization of bar based on counter++ could be a threading nightmare. (Have a look at the interlocked class for that.)

If ten simultaneous threads call this code, bar could end up with any old vale. A lock would stabilise things but then you've inserted this big blunt performance barrier without the user's say-so.

EDIT: new scenario added.

A comment by @greenoldman suggests that this simple example could be dealt with. But C# is full of syntactic sugar which is transformed to different 'basic' constructions. E.g., closures are converted to classes with fields, using statements become try/finally blocks, awaited calls become passed callbacks, and iterator methods become state machines.

So does the compiler have to handle any special cases when static variable initialization occurs? Are we confident that this would work?

async Task<int> UsesStatic(int defaultValue) 
{
    static int bar;
    try
    {
        throw new Exception("Boom!");
    }
    catch
    {
        using(var errorLogger = Log.NewLogger("init failed")
        {
            // here's the awaited call;
            bar = await service.LongRunningCall(() => Math.Abs(defaultValue));

            // that'll fail; 
            throw new Exception("Oh FFS!");
        }
    }
    finally
    {
        bar = 0;
    }
    return bar;
}

My guess is, the C# team looked at that, and thought 'that's a pure source of bugs', and left well alone.

Upvotes: 1

Jakub Lortz
Jakub Lortz

Reputation: 14896

It's actually very easy to check what the compiler would have to do to implement static variables in C#.

C# is designed to be compiled to CIL (Common Intermediate Language). C++, which supports static variables, can also be compiled to CIL.

Let's see what happens when we do it. First, let's consider the following simple class:

public ref class Class1
{
private:
    static int i = 0;

public:
    int M() {
        static int i = 0;
        i++;
        return i;
    }

    int M2() {
        i++;
        return i;
    }
};

}

Two methods, same behavior - i initialized to 0, incremented and returned each time the methods are called. Let's compare the IL.

.method public hidebysig instance int32  M() cil managed
{
  // Code size       20 (0x14)
  .maxstack  2
  .locals ([0] int32 V_0)
  IL_0000:  ldsfld     int32 '?i@?1??M@Class1@CppClassLibrary@@Q$AAMHXZ@4HA'
  IL_0005:  ldc.i4.1
  IL_0006:  add
  IL_0007:  stsfld     int32 '?i@?1??M@Class1@CppClassLibrary@@Q$AAMHXZ@4HA'
  IL_000c:  ldsfld     int32 '?i@?1??M@Class1@CppClassLibrary@@Q$AAMHXZ@4HA'
  IL_0011:  stloc.0
  IL_0012:  ldloc.0
  IL_0013:  ret
} // end of method Class1::M

.method public hidebysig instance int32  M2() cil managed
{
  // Code size       20 (0x14)
  .maxstack  2
  .locals ([0] int32 V_0)
  IL_0000:  ldsfld     int32 CppClassLibrary.Class1::i
  IL_0005:  ldc.i4.1
  IL_0006:  add
  IL_0007:  stsfld     int32 CppClassLibrary.Class1::i
  IL_000c:  ldsfld     int32 CppClassLibrary.Class1::i
  IL_0011:  stloc.0
  IL_0012:  ldloc.0
  IL_0013:  ret
} // end of method Class1::M2

The same. The only difference is the field name. It uses characters that are legal in CIL, but illegal in C++ so that the same name cannot be used in C++ code. C# compilers use this trick very often for auto-generated fields. The only difference is that the static variable cannot be accessed via reflection - I don't know how it's done.

Let's move to a more interesting example.

int M3(int a) {
    static int i = a;
    i++;
    return i;
}

Now the fun begins. The static variable cannot be initialized at compile-time anymore. It has to be done at run-time. And the compiler has to make sure it's only initialized once, so it has to be thread-safe.

The resulting CIL is

.method public hidebysig instance int32  M3(int32 a) cil managed
{
  // Code size       73 (0x49)
  .maxstack  2
  .locals ([0] int32 V_0)
  IL_0000:  ldsflda    int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'                                              
  IL_0005:  call       void _Init_thread_header_m(int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)*)
  IL_000a:  ldsfld     int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_000f:  ldc.i4.m1
  IL_0010:  bne.un.s   IL_0035
  .try
  {
    IL_0012:  ldarg.1
    IL_0013:  stsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
    IL_0018:  leave.s    IL_002b
  }  // end .try
  fault
  {
    IL_001a:  ldftn      void _Init_thread_abort_m(int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)*)
    IL_0020:  ldsflda    int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
    IL_0025:  call       void ___CxxCallUnwindDtor(method void *(void*),
                                                   void*)
    IL_002a:  endfinally
  }  // end handler
  IL_002b:  ldsflda    int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_0030:  call       void _Init_thread_footer_m(int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)*)
  IL_0035:  ldsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_003a:  ldc.i4.1
  IL_003b:  add
  IL_003c:  stsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_0041:  ldsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_0046:  stloc.0
  IL_0047:  ldloc.0
  IL_0048:  ret
} // end of method Class1::M3

Looks much more complicated. A second static field, something that looks like a critical section (although I can't find any information about the _Init_thread_* methods).

It doesn't look so trivial anymore. Performance suffers too. IMHO, it was a good decision not to implement static variables in C#.

To summarize,

To support static variables the C# compiler would have to:

  1. Create a private static field for the variable, making sure the name is unique and cannot be used directly in C# code.
  2. Make this field invisible via reflection.
  3. If the initialization cannot be done at compile-time, make it thread-safe.

It doesn't seem much, but if you combine several features like this one, the complexity rises exponentially.

And the only thing you get in return is an easy, compiler-provided, thread-safe initialization.

It's not a good idea to add a feature to a language only because other languages support it. Add the feature when it's really needed. The C# design team already made this mistake with array covariance

Upvotes: 3

Related Questions