Sometimes I need a hardcoded lookup table for a single method.
I can create such an array either
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?
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.
An array isn't immutable by nature; you can't make it constant.
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 .
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.
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#.
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.
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.
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
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
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 |
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