สิ่งที่สามารถอธิบายค่าใช้จ่ายในการใช้ 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

ตาม BenchmarkDotNet การใช้ค่า const ช้าลงอย่างมากและฉันไม่เข้าใจว่าทำไม

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 ไม่สำเร็จเมื่อทำงานใน Debug
ร่าเริง

อันที่จริงการใช้นาฬิกาจับเวลาพิสูจน์ให้เห็นว่าการใช้ const int นั้นช้ากว่าสนามใน a * a เพียงเล็กน้อยเท่านั้นแม้ว่ารหัส IL จะใช้ตัวถูกดำเนินการมากกว่า
Olivier Rogier

1
ใช้ BenchmarkDotNet 12.0 และ. Net Framework 4,8 ฉันรันโค้ดที่ถูกต้องจากคำถามและไม่เห็นความแตกต่างที่มีความหมายในผลลัพธ์ระหว่างสองวิธีเมื่อใช้ใน x86 ฉันเห็นความแตกต่างที่สังเกตได้เมื่อเปลี่ยนเป็น x64
Nineberry

cmpและmovคำแนะนำที่ใช้สำหรับเส้นทาง const ครอบครองหน่วยความจำมากกว่าคำแนะนำตามการลงทะเบียนเพราะการเข้ารหัสจำนวนต้องไบต์เพิ่มเติมและในเวลารวมมากขึ้นรอบการทำงานในการดำเนินการ (9 ไบต์ VS 5 ไบต์movและ 6 ไบต์ VS 5 ไบต์สำหรับ CMP) . และแม้ว่าจะมีmov ecx,dword ptr [rcx+10h]คำสั่งเพิ่มเติมสำหรับเวอร์ชั่นที่ไม่ใช่ const แต่ก็มีการปรับปรุงให้ดีที่สุดโดยคอมไพเลอร์ JIT ที่จะอยู่นอกลูปในเวอร์ชันรีลีส
Dmytro Mukalov

@DmytroMukalov แต่การเพิ่มประสิทธิภาพสำหรับเวอร์ชันที่ไม่ใช่ const จะทำให้การทำงานแบบขนานแตกต่างกันหรือไม่ วิธีสามารถคอมไพเลอร์ปรับให้เหมาะสมเมื่อตัวแปรสามารถเปลี่ยนแปลงได้ในเธรดที่แตกต่างกัน
ร่าเริง

คำตอบ:


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 |

ดูเหมือนจะไม่มีความแตกต่างระหว่างตัวแปรที่แตกต่างกัน ไม่ว่าทุกอย่างจะได้รับการปรับให้เหมาะสมหรือ const int ไม่ได้รับการปรับให้เหมาะสมไม่ว่าด้วยวิธีใดในสถานการณ์นี้


ฉันกำลังเล่นกับสิ่งนี้เช่นกันและฉันคิดว่าคุณกำลังจะทำอะไรบางอย่างดังนั้นขอบคุณสำหรับการป้อนข้อมูล หากอาร์เรย์อยู่รอดระหว่างการวัดประสิทธิภาพการคาดคะเนสาขาจะแตกต่างกันระหว่างสองกรณี ฉันจะกระตุ้นอีกรอบ
Brian Rasmussen

@BrianRasmussen ฉันคิดว่าหนึ่งความแตกต่างที่สำคัญคือเมื่ออาร์เรย์มีชีวิตอยู่กับค่าของมันเท่านั้นมาตรฐานแรกที่ทำงานจะต้องทำงานของการเปลี่ยนอาร์เรย์ สำหรับการวัดประสิทธิภาพเพิ่มเติมทั้งหมดในอาเรย์เดียวกันถ้าจะไม่เป็นจริง
Nineberry

@Ninerberry จุดดี หากการทดสอบส่วนใหญ่ทำงานด้วยค่าที่เปลี่ยนแปลงฉันยังไม่สามารถอธิบายความแตกต่างได้ แต่การตั้งค่าการทำซ้ำนั้นดูเหมือนจะมีความสำคัญ ขอบคุณทั้งคู่!
Brian Rasmussen

จริงๆแล้วประเด็นของฉันไม่ค่อยดีนัก เมื่อได้รับรหัสต้นฉบับในคำถามGlobalSetupจะถูกดำเนินการสองครั้งหนึ่งครั้งก่อนเกณฑ์มาตรฐานดังนั้นทั้งสองวิธีจะเริ่มต้นด้วยเงื่อนไขเดิม
Nineberry

@Nerryberry ใช่ แต่วิธีการแต่ละวิธีจะดำเนินการหลายครั้งเพื่อให้เรียบสุดขั้ว ดังนั้นสำหรับแต่ละวิธีจะมีการวนซ้ำหนึ่งครั้งที่ตกลงและการทำซ้ำอื่น ๆ ทั้งหมดที่ทำงานแตกต่างกัน
ร่าเริง
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.