이 경우 const 사용의 오버 헤드를 설명 할 수 있습니까?


9

여기 벽에 머리를 대고 있기 때문에 여러분 중 일부가 저를 교육시킬 수 있기를 바랍니다. 나는 BenchmarkDotNet을 사용하여 일부 성능 벤치 마크를 수행했으며 멤버를 선언하면 const성능이 크게 저하 되는 것처럼 보이는 이상한 경우가 발생했습니다 .

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;

namespace PerfTest
{
    [DisassemblyDiagnoser(printAsm: true, printSource: true)]
    public class Test
    {
        private int[] data;
        private int Threshold = 90;
        private const int ConstThreshold = 90;

        [GlobalSetup]
        public void GlobalSetup()
        {
            data = new int[1000];
            var random = new Random(42);
            for (var i = 0; i < data.Length; i++)
            {
                data[i] = random.Next(100);
            }
        }

        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Test>();
        }

        [Benchmark(Baseline = true)]
        public void ClampToMemberValue()
        {
            for (var i = 0; i < data.Length; i++)
            {
                if (data[i] > Threshold) data[i] = Threshold;
            }
        }

        [Benchmark]
        public void ClampToConstValue()
        {
            for (var i = 0; i < data.Length; i++)
            {
                if (data[i] > ConstThreshold) data[i] = ConstThreshold;
            }
        }
    }
}

두 테스트 방법의 유일한 차이점은 일반 멤버 변수와 const 멤버를 비교하는지 여부입니다.

constmark 값을 사용하는 BenchmarkDotNet에 따르면 상당히 느리고 이유를 이해할 수 없습니다.

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i7-5820K CPU 3.30GHz (Broadwell), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.0.100
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
  DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT


|             Method |     Mean |    Error |   StdDev | Ratio |
|------------------- |---------:|---------:|---------:|------:|
| ClampToMemberValue | 590.4 ns | 1.980 ns | 1.852 ns |  1.00 |
|  ClampToConstValue | 724.6 ns | 4.184 ns | 3.709 ns |  1.23 |

JIT 컴파일 된 코드를 살펴보면 내가 알 수있는 한 설명하지 않습니다. 다음은 두 가지 방법에 대한 코드입니다. 유일한 차이점은 비교가 레지스터 또는 리터럴과 비교되는지 여부입니다.

00007ff9`7f1b8500 PerfTest.Test.ClampToMemberValue()
            for (var i = 0; i < data.Length; i++)
                 ^^^^^^^^^
00007ff9`7f1b8504 33c0            xor     eax,eax
            for (var i = 0; i < data.Length; i++)
                            ^^^^^^^^^^^^^^^
00007ff9`7f1b8506 488b5108        mov     rdx,qword ptr [rcx+8]
00007ff9`7f1b850a 837a0800        cmp     dword ptr [rdx+8],0
00007ff9`7f1b850e 7e2e            jle     00007ff9`7f1b853e
00007ff9`7f1b8510 8b4910          mov     ecx,dword ptr [rcx+10h]
                if (data[i] > Threshold) data[i] = Threshold;
                ^^^^^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1b8513 4c8bc2          mov     r8,rdx
00007ff9`7f1b8516 458b4808        mov     r9d,dword ptr [r8+8]
00007ff9`7f1b851a 413bc1          cmp     eax,r9d
00007ff9`7f1b851d 7324            jae     00007ff9`7f1b8543
00007ff9`7f1b851f 4c63c8          movsxd  r9,eax
00007ff9`7f1b8522 43394c8810      cmp     dword ptr [r8+r9*4+10h],ecx
00007ff9`7f1b8527 7e0e            jle     00007ff9`7f1b8537
                if (data[i] > Threshold) data[i] = Threshold;
                                         ^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1b8529 4c8bc2          mov     r8,rdx
00007ff9`7f1b852c 448bc9          mov     r9d,ecx
00007ff9`7f1b852f 4c63d0          movsxd  r10,eax
00007ff9`7f1b8532 47894c9010      mov     dword ptr [r8+r10*4+10h],r9d
            for (var i = 0; i < data.Length; i++)
                                             ^^^
00007ff9`7f1b8537 ffc0            inc     eax
00007ff9`7f1b8539 394208          cmp     dword ptr [rdx+8],eax
00007ff9`7f1b853c 7fd5            jg      00007ff9`7f1b8513
        }
        ^
00007ff9`7f1b853e 4883c428        add     rsp,28h

00007ff9`7f1a8500 PerfTest.Test.ClampToConstValue()
            for (var i = 0; i < data.Length; i++)
                 ^^^^^^^^^
00007ff9`7f1a8504 33c0            xor     eax,eax
            for (var i = 0; i < data.Length; i++)
                            ^^^^^^^^^^^^^^^
00007ff9`7f1a8506 488b5108        mov     rdx,qword ptr [rcx+8]
00007ff9`7f1a850a 837a0800        cmp     dword ptr [rdx+8],0
00007ff9`7f1a850e 7e2d            jle     00007ff9`7f1a853d
                if (data[i] > ConstThreshold) data[i] = ConstThreshold;
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1a8510 488bca          mov     rcx,rdx
00007ff9`7f1a8513 448b4108        mov     r8d,dword ptr [rcx+8]
00007ff9`7f1a8517 413bc0          cmp     eax,r8d
00007ff9`7f1a851a 7326            jae     00007ff9`7f1a8542
00007ff9`7f1a851c 4c63c0          movsxd  r8,eax
00007ff9`7f1a851f 42837c81105a    cmp     dword ptr [rcx+r8*4+10h],5Ah
00007ff9`7f1a8525 7e0f            jle     00007ff9`7f1a8536
                if (data[i] > ConstThreshold) data[i] = ConstThreshold;
                                              ^^^^^^^^^^^^^^^^^^^^^^^^^
00007ff9`7f1a8527 488bca          mov     rcx,rdx
00007ff9`7f1a852a 4c63c0          movsxd  r8,eax
00007ff9`7f1a852d 42c74481105a000000 mov   dword ptr [rcx+r8*4+10h],5Ah
            for (var i = 0; i < data.Length; i++)
                                             ^^^
00007ff9`7f1a8536 ffc0            inc     eax
00007ff9`7f1a8538 394208          cmp     dword ptr [rdx+8],eax
00007ff9`7f1a853b 7fd3            jg      00007ff9`7f1a8510
        }
        ^
00007ff9`7f1a853d 4883c428        add     rsp,28h

내가 간과 한 것이 있다고 확신하지만이 시점에서 그것을 이해할 수 없으므로 이것을 설명 할 수있는 것에 대한 정보를 찾고 있습니다.


@OlivierRogier 디버그에서 실행할 때 BenchmarkDotNet이 실패한 것을 기억합니다.
Euphoric

실제로, 스톱워치를 사용하면 const int를 사용하는 것이 IL 코드가 더 많은 피연산자를 사용하더라도 간단한 a * a의 필드보다 약간 느리다는 것을 증명합니다.
Olivier Rogier

1
BenchmarkDotNet 12.0 및 .Net Framework 4,8을 사용하여 질문에서 정확한 코드를 실행하고 x86에서 실행할 때 두 방법 간의 결과에 의미있는 차이가 표시되지 않습니다. x64로 전환 할 때 관찰 된 차이를 볼 수 있습니다.
NineBerry

cmpmov숫자를 인코딩하는 것은 추가적인 바이트 많은 CPU 사이클을 실행하는 데 총 테이크 (5 바이트 대 9 바이트를 필요로하기 때문에 CONST 경로에 사용되는 설명은 레지스터 기반 명령어보다 더 많은 메모리를 점유 mov하고 CMP 5 바이트 VS 6 바이트) . mov ecx,dword ptr [rcx+10h]비 const 버전에 대한 추가 지침 이 있지만 JIT 컴파일러에 의해 릴리스 버전의 루프 외부에 있도록 최적화되었을 가능성이 큽니다.
Dmytro Mukalov

@DmytroMukalov 그러나 비 const 버전에 대한 최적화로 인해 병렬 실행에서 다르게 작동하지 않습니까? 변수가 다른 스레드에서 변경 될 수있을 때 컴파일러가 최적화하는 방법은 무엇입니까?
Euphoric

답변:


4

https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html을 보면

나는 당신이 [IterationSetup]대신 사용해야한다고 생각합니다 [GlobalSetup]. 전역 설정을 사용하면이 설정 data이 한 번 변경된 다음 data벤치 마크에서 재사용됩니다.

그래서 적절한 초기화를 사용하도록 코드를 변경했습니다. 검사를 더 자주 수행하도록 변수를 변경했습니다. 그리고 몇 가지 변형이 더 추가되었습니다.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;

namespace PerfTest
{
    [DisassemblyDiagnoser(printAsm: true, printSource: true)]
    public class Test
    {
        private int[] data;
        private int[] data_iteration;

        private int Threshold = 50;
        private const int ConstThreshold = 50;

        [GlobalSetup]
        public void GlobalSetup()
        {
            data = new int[100000];
            var random = new Random(42);
            for (var i = 0; i < data.Length; i++)
            {
                data[i] = random.Next(100);
            }
        }

        [IterationSetup]
        public void IterationSetup()
        {
            data_iteration = new int[data.Length];
            Array.Copy(data, data_iteration, data.Length);
        }

        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Test>();
        }

        [Benchmark]
        public void ClampToClassConstValue()
        {
            for (var i = 0; i < data_iteration.Length; i++)
            {
                if (data_iteration[i] > ConstThreshold) data_iteration[i] = ConstThreshold;
            }
        }

        [Benchmark]
        public void ClampToLocalConstValue()
        {
            const int ConstThresholdLocal = 50;
            for (var i = 0; i < data_iteration.Length; i++)
            {
                if (data_iteration[i] > ConstThresholdLocal) data_iteration[i] = ConstThresholdLocal;
            }
        }

        [Benchmark]
        public void ClampToInlineValue()
        {
            for (var i = 0; i < data_iteration.Length; i++)
            {
                if (data_iteration[i] > 50) data_iteration[i] = 50;
            }
        }

        [Benchmark]
        public void ClampToLocalVariable()
        {
            var ThresholdLocal = 50;
            for (var i = 0; i < data_iteration.Length; i++)
            {
                if (data_iteration[i] > ThresholdLocal) data_iteration[i] = ThresholdLocal;
            }
        }

        [Benchmark(Baseline = true)]
        public void ClampToMemberValue()
        {
            for (var i = 0; i < data_iteration.Length; i++)
            {
                if (data_iteration[i] > Threshold) data_iteration[i] = Threshold;
            }
        }
    }
}

더 정상적인 결과 :

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.17134.1069 (1803/April2018Update/Redstone4)
Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
Frequency=2531250 Hz, Resolution=395.0617 ns, Timer=TSC
.NET Core SDK=3.0.100
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  Job-INSHHX : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

InvocationCount=1  UnrollFactor=1

|                 Method |     Mean |    Error |   StdDev |   Median | Ratio | RatioSD |
|----------------------- |---------:|---------:|---------:|---------:|------:|--------:|
| ClampToClassConstValue | 391.5 us | 17.86 us | 17.54 us | 384.2 us |  1.02 |    0.05 |
| ClampToLocalConstValue | 399.6 us |  9.49 us | 11.66 us | 399.0 us |  1.05 |    0.07 |
|     ClampToInlineValue | 384.1 us |  5.99 us |  5.00 us | 383.0 us |  1.00 |    0.06 |
|   ClampToLocalVariable | 382.7 us |  3.60 us |  3.00 us | 382.0 us |  1.00 |    0.05 |
|     ClampToMemberValue | 379.6 us |  8.48 us | 16.73 us | 371.8 us |  1.00 |    0.00 |

서로 다른 변형 간에는 차이가없는 것 같습니다. 이 시나리오에서는 모든 것이 최적화되거나 어떤 식 으로든 최적화되지 않았습니다.


나는 이것도 가지고 놀고 있었고 당신이 무언가에 있다고 생각합니다. 입력에 감사드립니다. 배열이 벤치 마크간에 유지되는 경우 분기 예측은 두 경우간에 다릅니다. 좀 더 둘러 볼게요.
브라이언 라스무센

@BrianRasmussen 한 가지 주요 차이점은 어레이가 해당 값으로 살아남을 때 실행되는 첫 번째 벤치 마크 만 어레이 변경 작업을 수행해야한다는 것입니다. 동일한 어레이의 모든 추가 벤치 마크에서 if는 절대 적용되지 않습니다.
NineBerry

@NineBerry 좋은 지적입니다. 대부분의 테스트가 변경된 값으로 실행되는 경우 여전히 차이점을 설명 할 수는 없지만 반복 설정을 수행하는 것이 중요해 보이므로 여기에 무언가가 있습니다. 둘 다 고마워!
브라이언 라스무센

사실 내 요점은 그렇게 좋지 않았다. 문제의 원래 코드가 주어지면 GlobalSetup각 벤치 마크 전에 한 번 두 번 실행되므로 두 방법 모두 동일한 사전 조건으로 시작합니다.
NineBerry

@NineBerry 예. 그러나 각 방법은 극한을 부드럽게하는 방법으로 여러 번 실행됩니다. 따라서 각 방법마다 하나의 반복이 정상이고 다른 모든 반복이 다르게 작동합니다.
행복감
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.