Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

creating a constant but local array

Sometimes I need a hardcoded lookup table for a single method.

I can create such an array either

  • locally in the method itself
  • static inside the class

Example for the first case:

public int Convert(int i) {     int[] lookup = new[] {1, 2, 4, 8, 16, 32, 666, /*...*/ };     return lookup[i]; } 

As far as I understand it, a new lookup array will be created by the .net engine each time this method is executed. Is this correct, or is the JITer smart enough to cache and reuse the array between calls?

I presume that the answer is no, so if I want to make sure that the array is cached between calls, one way would be to make it static:

Example for the second case:

private static readonly int[] lookup = new[] { 1, 2, 4, 8, 16, 32, 666, /*...*/ }; public int Convert(int i) {     return lookup[i]; } 

Is there a way to do this without polluting the namespace of my class? Can I somehow declare a static array that is only visible inside the current scope?

like image 742
HugoRune Avatar asked Aug 23 '16 16:08

HugoRune


People also ask

How do you assign an array to constant?

In C#, use readonly to declare a const array. public static readonly string[] a = { "Car", "Motorbike", "Cab" }; In readonly, you can set the value at runtime as well unlike const.

Is it possible to define constant of an array type float32?

An array isn't immutable by nature; you can't make it constant.

Can an array be const C#?

Only the C# built-in types (excluding System. Object) may be declared as const . User-defined types, including classes, structs, and arrays, cannot be const .

How do I create FunFun behind the constants array?

Fun behind the constants array, is you can’t create it, because you can apply constant only a local values. However there is certain work around. Instead of constant array you can declare read only array, but all depends upon the requirement that you have. Here my array will be initialized first before use.

How to avoid this error when declaring a constant array?

This error can be avoided by using the readonly keyword in C#. The readonly keyword is used to specify that a variable’s value cannot be modified after initialization. The following code example shows us how we can declare a constant array with the readonly keyword in C#.

How to declare a constant array with the readonly keyword?

Declare a Constant Array With the readonly Keyword in C In C#, we cannot declare a constant array with the following syntax. public const string[] Values = { "Value1", "Value2", "Value3", "Value4" }; This will give a compiler error because the const keyword is used for values that are known at the compile-time.

Why do I get a compiler error when I const an array?

This will give a compiler error because the const keyword is used for values that are known at the compile-time. But an array does not get initialized during compile-time, so the array’s value is not known during the compile-time.


1 Answers

Local array

The Roslyn compiler puts local arrays in the metadata. Let's take the first version of your Convert method:

public int Convert(int i) {     int[] lookup = new[] {1, 2, 4, 8, 16, 32, 666, /*...*/ };     return lookup[i]; } 

Here is the corresponded IL code (Release build, Roslyn 1.3.1.60616):

// Token: 0x06000002 RID: 2 RVA: 0x0000206C File Offset: 0x0000026C .method public hidebysig      instance int32 Convert (         int32 i     ) cil managed noinlining  {     // Header Size: 1 byte     // Code Size: 20 (0x14) bytes     .maxstack 8      /* 0x0000026D 1D           */ IL_0000: ldc.i4.7     /* 0x0000026E 8D13000001   */ IL_0001: newarr    [mscorlib]System.Int32     /* 0x00000273 25           */ IL_0006: dup     /* 0x00000274 D001000004   */ IL_0007: ldtoken   field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=28' '<PrivateImplementationDetails>'::'502D7419C3650DEE94B5938147BC9B4724D37F99'     /* 0x00000279 281000000A   */ IL_000C: call      void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)     /* 0x0000027E 03           */ IL_0011: ldarg.1     /* 0x0000027F 94           */ IL_0012: ldelem.i4     /* 0x00000280 2A           */ IL_0013: ret } // end of method Program::Convert 

And here is the PrivateImplementationDetails:

// Token: 0x02000003 RID: 3 .class private auto ansi sealed '<PrivateImplementationDetails>'     extends [mscorlib]System.Object {     .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (         01 00 00 00     )     // Nested Types     // Token: 0x02000004 RID: 4     .class nested private explicit ansi sealed '__StaticArrayInitTypeSize=28'         extends [mscorlib]System.ValueType     {         .pack 1         .size 28      } // end of class __StaticArrayInitTypeSize=28       // Fields     // Token: 0x04000001 RID: 1 RVA: 0x00002944 File Offset: 0x00000B44     .field assembly static initonly valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=28' '502D7419C3650DEE94B5938147BC9B4724D37F99' at I_00002944 // 28 (0x001c) bytes  } // end of class <PrivateImplementationDetails> 

As you can see, your lookup array is in the assembly metadata. When you start your application, JIT only has to get the array content from the metadata. An asm example (Windows 10, .NET Framework 4.6.1 (4.0.30319.42000), RyuJIT: clrjit-v4.6.1080.0, Release build):

            int[] lookup = new[] { 1, 2, 4, 8, 16, 32, 666, /*...*/ }; 00007FFEDF0A44E2  sub         esp,20h   00007FFEDF0A44E5  mov         esi,edx   00007FFEDF0A44E7  mov         rcx,7FFF3D1C4C62h   00007FFEDF0A44F1  mov         edx,7   00007FFEDF0A44F6  call        00007FFF3E6B2600   00007FFEDF0A44FB  mov         rdx,134CF7F2944h   00007FFEDF0A4505  mov         ecx,dword ptr [rax+8]   00007FFEDF0A4508  lea         r8,[rax+10h]   00007FFEDF0A450C  vmovdqu     xmm0,xmmword ptr [rdx]   00007FFEDF0A4511  vmovdqu     xmmword ptr [r8],xmm0   00007FFEDF0A4516  mov         r9,qword ptr [rdx+10h]   00007FFEDF0A451A  mov         qword ptr [r8+10h],r9   00007FFEDF0A451E  mov         r9d,dword ptr [rdx+18h]   00007FFEDF0A4522  mov         dword ptr [r8+18h],r9d               return lookup[i]; 00007FFEDF0A4526  cmp         esi,ecx               return lookup[i]; 00007FFEDF0A4528  jae         00007FFEDF0A4537   00007FFEDF0A452A  movsxd      rdx,esi   00007FFEDF0A452D  mov         eax,dword ptr [rax+rdx*4+10h]   00007FFEDF0A4531  add         rsp,20h   00007FFEDF0A4535  pop         rsi   00007FFEDF0A4536  ret   00007FFEDF0A4537  call        00007FFF3EB57BE0   00007FFEDF0A453C  int         3   

A LegacyJIT-x64 version:

            int[] lookup = new[] { 1, 2, 4, 8, 16, 32, 666, /*...*/ }; 00007FFEDF0E41E0  push        rbx   00007FFEDF0E41E1  push        rdi   00007FFEDF0E41E2  sub         rsp,28h   00007FFEDF0E41E6  mov         ebx,edx   00007FFEDF0E41E8  mov         edx,7   00007FFEDF0E41ED  lea         rcx,[7FFF3D1C4C62h]   00007FFEDF0E41F4  call        00007FFF3E6B2600   00007FFEDF0E41F9  mov         rdi,rax   00007FFEDF0E41FC  lea         rcx,[7FFEDF124760h]   00007FFEDF0E4203  call        00007FFF3E73CA90   00007FFEDF0E4208  mov         rdx,rax   00007FFEDF0E420B  mov         rcx,rdi   00007FFEDF0E420E  call        00007FFF3E73C8B0               return lookup[i]; 00007FFEDF0E4213  movsxd      r11,ebx   00007FFEDF0E4216  mov         rax,qword ptr [rdi+8]   00007FFEDF0E421A  cmp         r11,7   00007FFEDF0E421E  jae         00007FFEDF0E4230   00007FFEDF0E4220  mov         eax,dword ptr [rdi+r11*4+10h]   00007FFEDF0E4225  add         rsp,28h   00007FFEDF0E4229  pop         rdi   00007FFEDF0E422A  pop         rbx   00007FFEDF0E422B  ret   00007FFEDF0E422C  nop         dword ptr [rax]   00007FFEDF0E4230  call        00007FFF3EB57BE0   00007FFEDF0E4235  nop   

A LegacyJIT-x86 version:

            int[] lookup = new[] { 1, 2, 4, 8, 16, 32, 666, /*...*/ }; 009A2DC4  push        esi   009A2DC5  push        ebx   009A2DC6  mov         ebx,edx   009A2DC8  mov         ecx,6A2C402Eh   009A2DCD  mov         edx,7   009A2DD2  call        0094322C   009A2DD7  lea         edi,[eax+8]   009A2DDA  mov         esi,5082944h   009A2DDF  mov         ecx,7   009A2DE4  rep movs    dword ptr es:[edi],dword ptr [esi]               return lookup[i]; 009A2DE6  cmp         ebx,dword ptr [eax+4]   009A2DE9  jae         009A2DF4   009A2DEB  mov         eax,dword ptr [eax+ebx*4+8]   009A2DEF  pop         ebx   009A2DF0  pop         esi   009A2DF1  pop         edi   009A2DF2  pop         ebp   009A2DF3  ret   009A2DF4  call        6B9D52F0   009A2DF9  int         3   

Static array

Now, let's compare it with the second version:

private static readonly int[] lookup = new[] { 1, 2, 4, 8, 16, 32, 666, /*...*/ };  public int Convert(int i) {                 return lookup[i]; } 

IL:

// Token: 0x04000001 RID: 1 .field private static initonly int32[] lookup  // Token: 0x06000002 RID: 2 RVA: 0x00002056 File Offset: 0x00000256 .method public hidebysig      instance int32 Convert (         int32 i     ) cil managed noinlining  {     // Header Size: 1 byte     // Code Size: 8 (0x8) bytes     .maxstack 8      /* 0x00000257 7E01000004   */ IL_0000: ldsfld    int32[] ConsoleApplication5.Program::lookup     /* 0x0000025C 03           */ IL_0005: ldarg.1     /* 0x0000025D 94           */ IL_0006: ldelem.i4     /* 0x0000025E 2A           */ IL_0007: ret } // end of method Program::Convert  // Token: 0x02000003 RID: 3 .class private auto ansi sealed '<PrivateImplementationDetails>'     extends [mscorlib]System.Object {     .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (         01 00 00 00     )     // Nested Types     // Token: 0x02000004 RID: 4     .class nested private explicit ansi sealed '__StaticArrayInitTypeSize=28'         extends [mscorlib]System.ValueType     {         .pack 1         .size 28      } // end of class __StaticArrayInitTypeSize=28       // Fields     // Token: 0x04000002 RID: 2 RVA: 0x000028FC File Offset: 0x00000AFC     .field assembly static initonly valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=28' '502D7419C3650DEE94B5938147BC9B4724D37F99' at I_000028fc // 28 (0x001c) bytes  } // end of class <PrivateImplementationDetails> 

ASM (RyuJIT-x64):

            return lookup[i]; 00007FFEDF0B4490  sub         rsp,28h   00007FFEDF0B4494  mov         rax,212E52E0080h   00007FFEDF0B449E  mov         rax,qword ptr [rax]   00007FFEDF0B44A1  mov         ecx,dword ptr [rax+8]   00007FFEDF0B44A4  cmp         edx,ecx   00007FFEDF0B44A6  jae         00007FFEDF0B44B4   00007FFEDF0B44A8  movsxd      rdx,edx   00007FFEDF0B44AB  mov         eax,dword ptr [rax+rdx*4+10h]   00007FFEDF0B44AF  add         rsp,28h   00007FFEDF0B44B3  ret   00007FFEDF0B44B4  call        00007FFF3EB57BE0   00007FFEDF0B44B9  int         3   

ASM (LegacyJIT-x64):

            return lookup[i]; 00007FFEDF0A4611  sub         esp,28h   00007FFEDF0A4614  mov         rcx,226CC5203F0h   00007FFEDF0A461E  mov         rcx,qword ptr [rcx]   00007FFEDF0A4621  movsxd      r8,edx   00007FFEDF0A4624  mov         rax,qword ptr [rcx+8]   00007FFEDF0A4628  cmp         r8,rax   00007FFEDF0A462B  jae         00007FFEDF0A4637   00007FFEDF0A462D  mov         eax,dword ptr [rcx+r8*4+10h]   00007FFEDF0A4632  add         rsp,28h   00007FFEDF0A4636  ret   00007FFEDF0A4637  call        00007FFF3EB57BE0   00007FFEDF0A463C  nop   

ASM (LegacyJIT-x86):

            return lookup[i]; 00AA2E18  push        ebp   00AA2E19  mov         ebp,esp   00AA2E1B  mov         eax,dword ptr ds:[03628854h]   00AA2E20  cmp         edx,dword ptr [eax+4]   00AA2E23  jae         00AA2E2B   00AA2E25  mov         eax,dword ptr [eax+edx*4+8]   00AA2E29  pop         ebp   00AA2E2A  ret   00AA2E2B  call        6B9D52F0   00AA2E30  int         3   

Benchmarks

Let's write a benchmark with help of BenchmarkDotNet

[Config(typeof(Config)), LegacyJitX86Job, LegacyJitX64Job, RyuJitX64Job, RPlotExporter] public class ArrayBenchmarks {     private static readonly int[] lookup = new[] {1, 2, 4, 8, 16, 32, 666, /*...*/};      [MethodImpl(MethodImplOptions.NoInlining)]     public int ConvertStatic(int i)     {         return lookup[i];     }      [MethodImpl(MethodImplOptions.NoInlining)]     public int ConvertLocal(int i)     {         int[] localLookup = new[] {1, 2, 4, 8, 16, 32, 666, /*...*/};         return localLookup[i];     }      [Benchmark]     public int Static()     {         int sum = 0;         for (int i = 0; i < 10001; i++)             sum += ConvertStatic(0);         return sum;     }      [Benchmark]     public int Local()     {         int sum = 0;         for (int i = 0; i < 10001; i++)             sum += ConvertLocal(0);         return sum;     }      private class Config : ManualConfig     {         public Config()         {             Add(new MemoryDiagnoser());                             Add(MarkdownExporter.StackOverflow);         }     } } 

Note that it's a synthetic toy benchmark which uses NoInlining for the Convert methods. We use it to show the difference between two methods. The real performance will depend on how you are using the Convert method in your code. My results:

Host Process Environment Information: BenchmarkDotNet.Core=v0.9.9.0 OS=Microsoft Windows NT 6.2.9200.0 Processor=Intel(R) Core(TM) i7-4702MQ CPU 2.20GHz, ProcessorCount=8 Frequency=2143474 ticks, Resolution=466.5324 ns, Timer=TSC CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT] GC=Concurrent Workstation JitModules=clrjit-v4.6.1586.0  Type=ArrayBenchmarks  Mode=Throughput     Method | Platform |       Jit |        Median |     StdDev |    Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op | ------- |--------- |---------- |-------------- |----------- |--------- |------ |------ |------------------- |  Static |      X64 | LegacyJit |    24.0243 us |  0.1590 us |        - |     - |     - |               1.07 |   Local |      X64 | LegacyJit | 2,068.1034 us | 33.7142 us | 1,089.00 |     - |     - |         436,603.02 |  Static |      X64 |    RyuJit |    20.7906 us |  0.2018 us |        - |     - |     - |               1.06 |   Local |      X64 |    RyuJit |    83.4041 us |  0.9993 us |   613.55 |     - |     - |         244,936.53 |  Static |      X86 | LegacyJit |    20.9957 us |  0.2267 us |        - |     - |     - |               1.01 |   Local |      X86 | LegacyJit |   167.6257 us |  1.3543 us |   431.43 |     - |     - |         172,121.77 | 

enter image description here

Conclusion

  • Does .NET cache hardcoded local arrays? Kind of: the Roslyn compiler put it in the metadata.
  • Do we have any overhead in this case? Unfortunately, yes: JIT will copy the array content from the metadata for each invocation; it will work longer than the case with a static array. Runtime also allocates objects and produce memory traffic.
  • Should we care about it? It depends. If it's a hot method and you want to achieve a good level of performance, you should use a static array. If it's a cold method which doesn't affect the application performance, you probably should write “good” source code and put the array in the method scope.
like image 157
AndreyAkinshin Avatar answered Sep 27 '22 20:09

AndreyAkinshin