creating a constant but local array

前端 未结 1 710
时光取名叫无心
时光取名叫无心 2021-02-02 10:05

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 c
1条回答
  •  旧时难觅i
    2021-02-02 10:31

    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 ''/'__StaticArrayInitTypeSize=28' ''::'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 ''
        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 ''/'__StaticArrayInitTypeSize=28' '502D7419C3650DEE94B5938147BC9B4724D37F99' at I_00002944 // 28 (0x001c) bytes
    
    } // end of class 
    

    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 ''
        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 ''/'__StaticArrayInitTypeSize=28' '502D7419C3650DEE94B5938147BC9B4724D37F99' at I_000028fc // 28 (0x001c) bytes
    
    } // end of class 
    

    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 |
    

    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.

    0 讨论(0)
提交回复
热议问题