Điều gì có thể giải thích chi phí sử dụng const trong trường hợp này?


9

Tôi đang đập đầu vào tường ở đây, vì vậy tôi hy vọng rằng một số bạn có thể giáo dục tôi. Tôi đã thực hiện một số điểm chuẩn hiệu suất bằng cách sử dụng BenchmarkDotNet và tôi gặp phải trường hợp kỳ lạ này khi dường như việc tuyên bố một thành viên constlàm giảm hiệu suất đáng kể.

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

Lưu ý rằng sự khác biệt duy nhất giữa hai phương thức kiểm tra là liệu chúng so sánh với biến thành viên thông thường hay thành viên const.

Theo BenchmarkDotNet, sử dụng giá trị const chậm hơn đáng kể và tôi không hiểu tại sao.

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 |

Nhìn vào mã được biên dịch JIT không giải thích được như tôi có thể nói. Đây là mã cho hai phương thức. Sự khác biệt duy nhất là việc so sánh được thực hiện so với đăng ký hay bằng chữ.

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

Tôi chắc chắn có điều gì đó tôi đã bỏ qua, nhưng tôi không thể mò mẫm nó vào thời điểm này vì vậy tôi đang tìm kiếm đầu vào về những gì có thể giải thích điều này.


@OlivierRogier Tôi nhớ rằng BenchmarkDotNet bị lỗi khi chạy trong Debug.
Euphoric

Thật vậy, việc sử dụng đồng hồ bấm giờ chứng minh rằng việc sử dụng const int chậm hơn một chút so với trường trên một * a ... đơn giản ngay cả khi mã IL sử dụng nhiều toán hạng hơn.
Olivier Rogier

1
Sử dụng BenchmarkDotNet 12.0 và .Net Framework 4,8, tôi thực thi mã chính xác từ câu hỏi và không thấy bất kỳ sự khác biệt có ý nghĩa nào trong kết quả giữa hai phương thức khi chạy trong x86. Tôi có thể thấy sự khác biệt quan sát được khi chuyển sang x64.
Nine tweet

Các cmpmovhướng dẫn được sử dụng cho đường dẫn const chiếm nhiều bộ nhớ hơn hướng dẫn dựa một thanh ghi vì mã hóa một số đòi hỏi byte bổ sung và tổng cộng mất hơn chu kỳ CPU để thực hiện (9 byte vs 5 byte cho movvà 6 byte vs 5 byte cho CMP) . Và mặc dù có mov ecx,dword ptr [rcx+10h]hướng dẫn bổ sung cho phiên bản không phải là const, trình biên dịch JIT được tối ưu hóa nhất nằm ngoài vòng lặp trong phiên bản phát hành.
Dmytro Mukalov

@DmytroMukalov Nhưng việc tối ưu hóa cho phiên bản không phải const sẽ khiến nó hoạt động khác đi trong thực thi song song? Làm thế nào trình biên dịch có thể tối ưu hóa nó khi biến có thể được thay đổi trong các luồng khác nhau.
Euphoric

Câu trả lời:


4

Nhìn vào https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html

Tôi tin rằng bạn nên sử dụng [IterationSetup]thay vì [GlobalSetup]. Với thiết lập toàn cầu, cái datađược thay đổi một lần và sau đó thay đổi datađược sử dụng lại qua các điểm chuẩn.

Vì vậy, tôi đã thay đổi mã để sử dụng khởi tạo thích hợp. Thay đổi các biến để kiểm tra thường xuyên hơn. Và thêm một vài biến thể.

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

Kết quả trông bình thường hơn:

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 |

Dường như không có sự khác biệt giữa các biến thể khác nhau. Mọi thứ đều được tối ưu hóa hoặc const int không được tối ưu hóa theo bất kỳ cách nào trong kịch bản này.


Tôi cũng đang chơi xung quanh với điều này và tôi nghĩ rằng bạn đang tham gia vào một cái gì đó, vì vậy cảm ơn cho đầu vào. Nếu mảng tồn tại giữa các điểm chuẩn thì dự đoán nhánh sẽ khác nhau giữa hai trường hợp. Tôi sẽ chọc xung quanh một số nữa.
Brian Rasmussen

@BrianRasmussen Tôi nghĩ một điểm khác biệt chính là khi mảng tồn tại với các giá trị của nó, chỉ có điểm chuẩn đầu tiên chạy phải thực hiện công việc thay đổi mảng. Đối với tất cả các điểm chuẩn tiếp theo trên cùng một mảng, nếu không bao giờ là đúng.
Nine tweet

@Nine BlackBerry điểm tốt. Nếu phần lớn các thử nghiệm chạy với các giá trị bị thay đổi, tôi vẫn không thể giải thích được sự khác biệt, nhưng thực hiện thiết lập lặp có vẻ như là vấn đề nên có điều gì đó ở đây để nghiên cứu. Cảm ơn cả hai bạn!
Brian Rasmussen

Thật ra quan điểm của tôi không tốt lắm. Với mã gốc trong câu hỏi, GlobalSetupđược thực thi hai lần, một lần trước mỗi Điểm chuẩn, vì vậy cả hai phương thức đều bắt đầu với cùng một điều kiện trước.
Nine tweet

@Nine BlackBerry Có. Nhưng mỗi phương thức được thực thi nhiều lần như là cách để làm dịu các thái cực. Vì vậy, đối với mỗi phương thức, có một lần lặp là OK và sau đó tất cả các lần lặp khác có hành vi khác nhau.
Euphoric
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.