Apa yang bisa menjelaskan overhead menggunakan const dalam kasus ini?

9

Saya membenturkan kepala ke dinding, jadi saya harap beberapa dari Anda dapat mendidik saya. Saya sedang melakukan beberapa tolok ukur kinerja menggunakan BenchmarkDotNet dan saya mengalami kasus aneh ini di mana tampaknya menyatakan bahwa anggota sangat constmenurunkan kinerja.

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;
            }
        }
    }
}

Perhatikan bahwa satu-satunya perbedaan antara kedua metode pengujian adalah apakah mereka membandingkan terhadap variabel anggota biasa atau anggota const.

Menurut BenchmarkDotNet menggunakan nilai const secara signifikan lebih lambat dan saya tidak mengerti mengapa.

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 |

Melihat kode kompilasi JIT tidak menjelaskan sejauh yang saya tahu. Berikut kode untuk kedua metode tersebut. Satu-satunya perbedaan adalah apakah perbandingan dilakukan terhadap register atau literal.

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

dan

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

Saya yakin ada sesuatu yang saya abaikan, tetapi saya tidak bisa mengatasinya pada saat ini sehingga saya mencari masukan tentang apa yang bisa menjelaskan hal ini.

Brian Rasmussen
sumber
@OlivierRogier Saya ingat BenchmarkDotNet gagal ketika dijalankan di Debug.
Euforia
Memang, menggunakan stopwatch membuktikan bahwa menggunakan const int sedikit lebih lambat daripada bidang pada a * a sederhana ... bahkan jika kode IL menggunakan lebih banyak operan.
Olivier Rogier
1
Menggunakan BenchmarkDotNet 12.0 dan .Net Framework 4,8, saya mengeksekusi kode yang tepat dari pertanyaan dan tidak melihat perbedaan yang berarti dalam hasil antara kedua metode saat berjalan di x86. Saya bisa melihat perbedaan yang diamati ketika beralih ke x64.
NineBerry
The cmpdan movpetunjuk yang digunakan untuk jalur const menempati memori lebih dari petunjuk berdasarkan register karena pengkodean nomor memerlukan byte tambahan dan total take lebih siklus CPU untuk mengeksekusi (9 byte vs 5 byte untuk movdan 6 byte vs 5 bytes untuk CMP) . Dan meskipun ada mov ecx,dword ptr [rcx+10h]instruksi tambahan untuk versi non-const itu kemungkinan besar dioptimalkan oleh JIT compiler untuk berada di luar loop dalam versi rilis.
Dmytro Mukalov
@ DmytroMukalov Tapi bukankah optimasi untuk versi non-const menyebabkannya berperilaku berbeda dalam eksekusi paralel? Bagaimana kompilator mengoptimalkannya ketika variabel dapat diubah di utas yang berbeda.
Euforia

Jawaban:

4

Melihat https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html

Saya percaya Anda harus menggunakan [IterationSetup]bukan [GlobalSetup]. Dengan pengaturan global, dataperubahan diubah satu kali dan kemudian perubahan datatersebut digunakan kembali di seluruh tolok ukur.

Jadi, saya telah mengubah kode untuk menggunakan inisialisasi yang tepat. Variabel yang diubah untuk membuat pemeriksaan lebih sering. Dan menambahkan beberapa variasi lagi.

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;
            }
        }
    }
}

Hasil terlihat lebih normal:

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 |

Sepertinya tidak ada perbedaan antara varian yang berbeda. Baik semuanya dioptimalkan atau konstan tidak dioptimalkan dengan cara apa pun dalam skenario ini.

Euforia
sumber
Saya juga bermain-main dengan ini dan saya pikir Anda sedang melakukan sesuatu, jadi terima kasih atas masukannya. Jika array bertahan di antara tolok ukur maka prediksi cabang akan berbeda antara dua kasus. Saya akan melihat-lihat lagi.
Brian Rasmussen
@ BrianRasmussen Saya pikir satu perbedaan utama adalah bahwa ketika array bertahan dengan nilainya, hanya tolok ukur pertama yang berjalan harus melakukan pekerjaan mengubah array. Untuk semua tolok ukur lebih lanjut pada array yang sama, if tidak akan pernah benar.
NineBerry
@NineBerry poin bagus. Jika sebagian besar tes berjalan dengan nilai-nilai yang diubah saya masih tidak dapat menjelaskan perbedaannya, tetapi melakukan pengaturan iterasi tampaknya menjadi masalah sehingga ada sesuatu di sini untuk digali. Terima kasih kalian berdua!
Brian Rasmussen
Sebenarnya maksud saya tidak begitu baik. Mengingat kode asli dalam pertanyaan, GlobalSetupdijalankan dua kali, satu kali sebelum masing-masing Tolok Ukur, sehingga kedua metode dimulai dengan prasyarat yang sama.
NineBerry
@NineBerry Ya. Tetapi setiap metode dieksekusi berkali-kali sebagai cara untuk menghaluskan ekstrem. Jadi untuk setiap metode ada satu iterasi yang OK dan kemudian semua iterasi lain yang berperilaku berbeda.
Euforia