Почему суммирование массива типов значений медленнее, чем суммирование массива ссылочных типов? - PullRequest
0 голосов
/ 09 декабря 2018

Я пытаюсь лучше понять, как работает память в .NET, поэтому я играю с BenchmarkDotNet и диагностическими инструментами .Я создал тест, сравнивающий производительность class и struct путем суммирования элементов массива.Я ожидал, что суммирование типов значений всегда будет быстрее.Но для коротких массивов это не так.Кто-нибудь может объяснить это?

Код:

internal class ReferenceType
{
    public int Value;
}

internal struct ValueType
{
    public int Value;
}

internal struct ExtendedValueType
{
    public int Value;
    private double _otherData; // this field is here just to make the object bigger
}

У меня есть три массива:

    private ReferenceType[] _referenceTypeData;
    private ValueType[] _valueTypeData;
    private ExtendedValueType[] _extendedValueTypeData;

, которые я инициализирую с тем же набором случайных значений.

Затем сравнительный метод:

    [Benchmark]
    public int ReferenceTypeSum()
    {
        var sum = 0;

        for (var i = 0; i < Size; i++)
        {
            sum += _referenceTypeData[i].Value;
        }

        return sum;
    }

Size является параметром сравнения.Два других метода сравнения (ValueTypeSum и ExtendedValueTypeSum) идентичны, за исключением того, что я суммирую по _valueTypeData или _extendedValueTypeData. Полный код для теста .

Результаты теста:

DefaultJob: .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64-битная версия RyuJIT-v4.7.3190.0

               Method | Size |      Mean |     Error |    StdDev | Ratio | RatioSD |
--------------------- |----- |----------:|----------:|----------:|------:|--------:|
     ReferenceTypeSum |  100 |  75.76 ns | 1.2682 ns | 1.1863 ns |  1.00 |    0.00 |
         ValueTypeSum |  100 |  79.83 ns | 0.3866 ns | 0.3616 ns |  1.05 |    0.02 |
 ExtendedValueTypeSum |  100 |  78.70 ns | 0.8791 ns | 0.8223 ns |  1.04 |    0.01 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum |  500 | 354.78 ns | 3.9368 ns | 3.6825 ns |  1.00 |    0.00 |
         ValueTypeSum |  500 | 367.08 ns | 5.2446 ns | 4.9058 ns |  1.03 |    0.01 |
 ExtendedValueTypeSum |  500 | 346.18 ns | 2.1114 ns | 1.9750 ns |  0.98 |    0.01 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum | 1000 | 697.81 ns | 6.8859 ns | 6.1042 ns |  1.00 |    0.00 |
         ValueTypeSum | 1000 | 720.64 ns | 5.5592 ns | 5.2001 ns |  1.03 |    0.01 |
 ExtendedValueTypeSum | 1000 | 699.12 ns | 9.6796 ns | 9.0543 ns |  1.00 |    0.02 |

Core: .NET Core 2.1.4 (CoreCLR 4.6.26814.03, CoreFX 4.6.26814.02), 64-битный RyuJIT

               Method | Size |      Mean |     Error |    StdDev | Ratio | RatioSD |
--------------------- |----- |----------:|----------:|----------:|------:|--------:|
     ReferenceTypeSum |  100 |  76.22 ns | 0.5232 ns | 0.4894 ns |  1.00 |    0.00 |
         ValueTypeSum |  100 |  80.69 ns | 0.9277 ns | 0.8678 ns |  1.06 |    0.01 |
 ExtendedValueTypeSum |  100 |  78.88 ns | 1.5693 ns | 1.4679 ns |  1.03 |    0.02 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum |  500 | 354.30 ns | 2.8682 ns | 2.5426 ns |  1.00 |    0.00 |
         ValueTypeSum |  500 | 372.72 ns | 4.2829 ns | 4.0063 ns |  1.05 |    0.01 |
 ExtendedValueTypeSum |  500 | 357.50 ns | 7.0070 ns | 6.5543 ns |  1.01 |    0.02 |
                      |      |           |           |           |       |         |
     ReferenceTypeSum | 1000 | 696.75 ns | 4.7454 ns | 4.4388 ns |  1.00 |    0.00 |
         ValueTypeSum | 1000 | 697.95 ns | 2.2462 ns | 2.1011 ns |  1.00 |    0.01 |
 ExtendedValueTypeSum | 1000 | 687.75 ns | 2.3861 ns | 1.9925 ns |  0.99 |    0.01 |

Я запустил эталонный тест с аппаратными счетчиками BranchMispredictions и CacheMisses, но нет ошибок в кеше и ветвлениях.Я также проверил код выпуска IL, и методы тестирования отличаются только инструкциями, которые загружают ссылочные переменные или переменные типа значения.

Для больших размеров массива суммирование массива типа значения всегда быстрее (например, потому что типы значения занимают меньше памяти), но я не понимаю, почему он медленнее для более коротких массивов.Что мне здесь не хватает?И почему увеличение struct (см. ExtendedValueType) делает суммирование немного быстрее?

---- ОБНОВЛЕНИЕ ----

Вдохновлено комментарием, сделанным @usr Iперезапустил тест с LegacyJit.Я также добавил диагностический модуль памяти, вдохновленный @Silver Shroud (да, выделений кучи нет).

Job = LegacyJitX64 Jit = LegacyJit Platform = X64 Runtime = Clr

               Method | Size |       Mean |      Error |     StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
--------------------- |----- |-----------:|-----------:|-----------:|------:|--------:|------------:|------------:|------------:|--------------------:|
     ReferenceTypeSum |  100 |   110.1 ns |  0.6836 ns |  0.6060 ns |  1.00 |    0.00 |           - |           - |           - |                   - |
         ValueTypeSum |  100 |   109.5 ns |  0.4320 ns |  0.4041 ns |  0.99 |    0.00 |           - |           - |           - |                   - |
 ExtendedValueTypeSum |  100 |   109.5 ns |  0.5438 ns |  0.4820 ns |  0.99 |    0.00 |           - |           - |           - |                   - |
                      |      |            |            |            |       |         |             |             |             |                     |
     ReferenceTypeSum |  500 |   517.8 ns | 10.1271 ns | 10.8359 ns |  1.00 |    0.00 |           - |           - |           - |                   - |
         ValueTypeSum |  500 |   511.9 ns |  7.8204 ns |  7.3152 ns |  0.99 |    0.03 |           - |           - |           - |                   - |
 ExtendedValueTypeSum |  500 |   534.7 ns |  3.0168 ns |  2.8219 ns |  1.03 |    0.02 |           - |           - |           - |                   - |
                      |      |            |            |            |       |         |             |             |             |                     |
     ReferenceTypeSum | 1000 | 1,058.3 ns |  8.8829 ns |  8.3091 ns |  1.00 |    0.00 |           - |           - |           - |                   - |
         ValueTypeSum | 1000 | 1,048.4 ns |  8.6803 ns |  8.1196 ns |  0.99 |    0.01 |           - |           - |           - |                   - |
 ExtendedValueTypeSum | 1000 | 1,057.5 ns |  5.9456 ns |  5.5615 ns |  1.00 |    0.01 |           - |           - |           - |                   - |

С устаревшими результатами JIT, как и ожидалось, - но медленнее, чем предыдущие результаты !.Что говорит о том, что RyuJit делает некоторые магические улучшения производительности, которые лучше справляются с ссылочными типами.

---- ОБНОВЛЕНИЕ 2 ----

Спасибо за отличные ответы!Я многому научился!

Ниже приведены результаты еще одного теста.Я сравниваю изначально проверенные методы, оптимизированные методы, как предложено @usr и @xoofx:

[Benchmark]
public int ReferenceTypeOptimizedSum()
{
    var sum = 0;
    var array = _referenceTypeData;

    for (var i = 0; i < array.Length; i++)
    {
        sum += array[i].Value;
    }

    return sum;
} 

, и развернутые версии, как предложено @AndreyAkinshin, с добавленной выше оптимизацией:

[Benchmark]
public int ReferenceTypeUnrolledSum()
{
    var sum = 0;
    var array = _referenceTypeData;

    for (var i = 0; i < array.Length; i += 16)
    {
        sum += array[i].Value;
        sum += array[i + 1].Value;
        sum += array[i + 2].Value;
        sum += array[i + 3].Value;
        sum += array[i + 4].Value;
        sum += array[i + 5].Value;
        sum += array[i + 6].Value;
        sum += array[i + 7].Value;
        sum += array[i + 8].Value;
        sum += array[i + 9].Value;
        sum += array[i + 10].Value;
        sum += array[i + 11].Value;
        sum += array[i + 12].Value;
        sum += array[i + 13].Value;
        sum += array[i + 14].Value;
        sum += array[i + 15].Value;
    }

    return sum;
}

Полный код здесь.

Результаты тестов:

BenchmarkDotNet = v0.11.3, OS = Windows 10.0.17134.345 (1803 / April2018Update / Redstone4)) Процессор Intel Core i5-6400 2,70 ГГц (Skylake), 1 процессор, 4 логических и 4 физических ядра. Частота = 2648439 Гц, разрешение = 377,5809 нс, таймер = TSC

DefaultJob:.NET Framework 4.7.2 (CLR 4.0.30319.42000), 64-разрядная версия RyuJIT-v4.7.3190.0

                        Method | Size |     Mean |     Error |    StdDev | Ratio | RatioSD |
------------------------------ |----- |---------:|----------:|----------:|------:|--------:|
              ReferenceTypeSum |  512 | 344.8 ns | 3.6473 ns | 3.4117 ns |  1.00 |    0.00 |
                  ValueTypeSum |  512 | 361.2 ns | 3.8004 ns | 3.3690 ns |  1.05 |    0.02 |
          ExtendedValueTypeSum |  512 | 347.2 ns | 5.9686 ns | 5.5831 ns |  1.01 |    0.02 |

     ReferenceTypeOptimizedSum |  512 | 254.5 ns | 2.4427 ns | 2.2849 ns |  0.74 |    0.01 |
         ValueTypeOptimizedSum |  512 | 353.0 ns | 1.9201 ns | 1.7960 ns |  1.02 |    0.01 |
 ExtendedValueTypeOptimizedSum |  512 | 280.3 ns | 1.2423 ns | 1.0374 ns |  0.81 |    0.01 |

      ReferenceTypeUnrolledSum |  512 | 213.2 ns | 1.2483 ns | 1.1676 ns |  0.62 |    0.01 |
          ValueTypeUnrolledSum |  512 | 201.3 ns | 0.6720 ns | 0.6286 ns |  0.58 |    0.01 |
  ExtendedValueTypeUnrolledSum |  512 | 223.6 ns | 1.0210 ns | 0.9550 ns |  0.65 |    0.01 |

Ответы [ 4 ]

0 голосов
/ 10 декабря 2018

В Haswell корпорация Intel представила дополнительные стратегии прогнозирования ветвлений для небольших циклов (поэтому мы не можем наблюдать эту ситуацию на IvyBridge).Кажется, что конкретная стратегия ветвления зависит от многих факторов, включая выравнивание собственного кода.Различие между LegacyJIT и RyuJIT можно объяснить разными стратегиями выравнивания для методов.К сожалению, я не могу предоставить всю необходимую информацию об этих явлениях производительности (Intel хранит детали реализации в секрете; мои выводы основаны только на моих собственных экспериментах по реверс-инжинирингу ЦП), но я могу рассказать вам, как сделать этот тест лучше.

Основным приемом, который улучшает ваши результаты, является ручное развертывание цикла, что очень важно для нанобенчмарков на Haswell + с RyuJIT.Вышеуказанные явления затрагивают только маленькие петли, поэтому мы можем решить проблему с помощью огромного тела петли.Фактически, когда у вас есть эталонный тест, такой как

[Benchmark]
public void MyBenchmark()
{
    Foo();
}

, BenchmarkDotNet генерирует следующий цикл:

for (int i = 0; i < N; i++)
{
    Foo(); Foo(); Foo(); Foo();
    Foo(); Foo(); Foo(); Foo();
    Foo(); Foo(); Foo(); Foo();
    Foo(); Foo(); Foo(); Foo();
}

Вы можете контролировать количество внутренних вызовов в этом цикле с помощью UnrollFactor.Если у вас есть собственный маленький цикл внутри теста, вы должны развернуть его таким же образом:

[Benchmark(Baseline = true)]
public int ReferenceTypeSum()
{
    var sum = 0;

    for (var i = 0; i < Size; i += 16)
    {
        sum += _referenceTypeData[i].Value;
        sum += _referenceTypeData[i + 1].Value;
        sum += _referenceTypeData[i + 2].Value;
        sum += _referenceTypeData[i + 3].Value;
        sum += _referenceTypeData[i + 4].Value;
        sum += _referenceTypeData[i + 5].Value;
        sum += _referenceTypeData[i + 6].Value;
        sum += _referenceTypeData[i + 7].Value;
        sum += _referenceTypeData[i + 8].Value;
        sum += _referenceTypeData[i + 9].Value;
        sum += _referenceTypeData[i + 10].Value;
        sum += _referenceTypeData[i + 11].Value;
        sum += _referenceTypeData[i + 12].Value;
        sum += _referenceTypeData[i + 13].Value;
        sum += _referenceTypeData[i + 14].Value;
        sum += _referenceTypeData[i + 15].Value;
    }

    return sum;
}

Еще один трюк - агрессивный прогрев (например, 30 итераций).Вот так выглядит этап прогрева на моей машине:

WorkloadWarmup   1: 4194304 op, 865744000.00 ns, 206.4095 ns/op
WorkloadWarmup   2: 4194304 op, 892164000.00 ns, 212.7085 ns/op
WorkloadWarmup   3: 4194304 op, 861913000.00 ns, 205.4961 ns/op
WorkloadWarmup   4: 4194304 op, 868044000.00 ns, 206.9578 ns/op
WorkloadWarmup   5: 4194304 op, 933894000.00 ns, 222.6577 ns/op
WorkloadWarmup   6: 4194304 op, 890567000.00 ns, 212.3277 ns/op
WorkloadWarmup   7: 4194304 op, 923509000.00 ns, 220.1817 ns/op
WorkloadWarmup   8: 4194304 op, 861953000.00 ns, 205.5056 ns/op
WorkloadWarmup   9: 4194304 op, 862454000.00 ns, 205.6251 ns/op
WorkloadWarmup  10: 4194304 op, 862565000.00 ns, 205.6515 ns/op
WorkloadWarmup  11: 4194304 op, 867301000.00 ns, 206.7807 ns/op
WorkloadWarmup  12: 4194304 op, 841892000.00 ns, 200.7227 ns/op
WorkloadWarmup  13: 4194304 op, 827717000.00 ns, 197.3431 ns/op
WorkloadWarmup  14: 4194304 op, 828257000.00 ns, 197.4719 ns/op
WorkloadWarmup  15: 4194304 op, 812275000.00 ns, 193.6615 ns/op
WorkloadWarmup  16: 4194304 op, 792011000.00 ns, 188.8301 ns/op
WorkloadWarmup  17: 4194304 op, 792607000.00 ns, 188.9722 ns/op
WorkloadWarmup  18: 4194304 op, 794428000.00 ns, 189.4064 ns/op
WorkloadWarmup  19: 4194304 op, 794879000.00 ns, 189.5139 ns/op
WorkloadWarmup  20: 4194304 op, 794914000.00 ns, 189.5223 ns/op
WorkloadWarmup  21: 4194304 op, 794061000.00 ns, 189.3189 ns/op
WorkloadWarmup  22: 4194304 op, 793385000.00 ns, 189.1577 ns/op
WorkloadWarmup  23: 4194304 op, 793851000.00 ns, 189.2688 ns/op
WorkloadWarmup  24: 4194304 op, 793456000.00 ns, 189.1747 ns/op
WorkloadWarmup  25: 4194304 op, 794194000.00 ns, 189.3506 ns/op
WorkloadWarmup  26: 4194304 op, 793980000.00 ns, 189.2996 ns/op
WorkloadWarmup  27: 4194304 op, 804402000.00 ns, 191.7844 ns/op
WorkloadWarmup  28: 4194304 op, 801002000.00 ns, 190.9738 ns/op
WorkloadWarmup  29: 4194304 op, 797860000.00 ns, 190.2246 ns/op
WorkloadWarmup  30: 4194304 op, 802668000.00 ns, 191.3710 ns/op

По умолчанию BenchmarkDotNet пытается обнаружить такие ситуации и увеличить количество итераций прогрева.К сожалению, это не всегда возможно (при условии, что мы хотим провести «быструю» стадию прогрева в «простых» случаях).

А вот мои результаты (полный список обновленных тестов можно найти здесь: https://gist.github.com/AndreyAkinshin/4c9e0193912c99c0b314359d5c5d0a4e):

BenchmarkDotNet=v0.11.3, OS=macOS Mojave 10.14.1 (18B75) [Darwin 18.2.0]
Intel Core i7-4870HQ CPU 2.50GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100-preview-009812
  [Host]     : .NET Core 2.0.5 (CoreCLR 4.6.0.0, CoreFX 4.6.26018.01), 64bit RyuJIT
  Job-IHBGGW : .NET Core 2.0.5 (CoreCLR 4.6.0.0, CoreFX 4.6.26018.01), 64bit RyuJIT

IterationCount=30  WarmupCount=30  

               Method | Size |     Mean |     Error |    StdDev |   Median | Ratio | RatioSD |
--------------------- |----- |---------:|----------:|----------:|---------:|------:|--------:|
     ReferenceTypeSum |  256 | 180.7 ns | 0.4514 ns | 0.6474 ns | 180.8 ns |  1.00 |    0.00 |
         ValueTypeSum |  256 | 154.4 ns | 1.8844 ns | 2.8205 ns | 153.3 ns |  0.86 |    0.02 |
 ExtendedValueTypeSum |  256 | 183.1 ns | 2.2283 ns | 3.3352 ns | 181.1 ns |  1.01 |    0.02 |
0 голосов
/ 10 декабря 2018

Это действительно очень странное поведение.

Сгенерированный код для основного цикла для ссылочного типа выглядит следующим образом:

M00_L00:
mov     r9,rcx
cmp     edx,[r9+8]
jae     ArrayOutOfBound
movsxd  r10,edx
mov     r9,[r9+r10*8+10h]
add     eax,[r9+8]
inc     edx
cmp     edx,r8d
jl      M00_L00

в то время как для цикла типа значения:

M00_L00:
mov     r9,rcx
cmp     edx,[r9+8]
jae     ArrayOutOfBound
movsxd  r10,edx
add     eax,[r9+r10*4+10h]
inc     edx
cmp     edx,r8d
jl      M00_L00

Таким образом, разница сводится к:

Для тип ссылки :

mov     r9,[r9+r10*8+10h]
add     eax,[r9+8]

Для тип значения :

add     eax,[r9+r10*4+10h]

С одной инструкцией и без косвенного доступа к памяти тип значения должен быть быстрее ...

Я попытался выполнить это через Intel Architecture Code Analyzer и вывод IACAдля ссылочный тип равен:

Throughput Analysis Report
--------------------------
Block Throughput: 1.72 Cycles       Throughput Bottleneck: Dependency chains
Loop Count:  35
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  1.0     0.0  |  1.0  |  1.5     1.5  |  1.5     1.5  |  0.0  |  1.0  |  1.0  |  0.0  |
--------------------------------------------------------------------------------------------------

DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of   |                    Ports pressure in cycles                         |      |
|  Uops    |  0  - DV    |  1   |  2  -  D    |  3  -  D    |  4   |  5   |  6   |  7   |
-----------------------------------------------------------------------------------------
|   1*     |             |      |             |             |      |      |      |      | mov r9, rcx
|   2^     |             |      | 0.5     0.5 | 0.5     0.5 |      | 1.0  |      |      | cmp edx, dword ptr [r9+0x8]
|   0*F    |             |      |             |             |      |      |      |      | jnb 0x22
|   1      |             |      |             |             |      |      | 1.0  |      | movsxd r10, edx
|   1      |             |      | 0.5     0.5 | 0.5     0.5 |      |      |      |      | mov r9, qword ptr [r9+r10*8+0x10]
|   2^     | 1.0         |      | 0.5     0.5 | 0.5     0.5 |      |      |      |      | add eax, dword ptr [r9+0x8]
|   1      |             | 1.0  |             |             |      |      |      |      | inc edx
|   1*     |             |      |             |             |      |      |      |      | cmp edx, r8d
|   0*F    |             |      |             |             |      |      |      |      | jl 0xffffffffffffffe6
Total Num Of Uops: 9

Для тип значения :

Throughput Analysis Report
--------------------------
Block Throughput: 1.74 Cycles       Throughput Bottleneck: Dependency chains
Loop Count:  26
Port Binding In Cycles Per Iteration:
--------------------------------------------------------------------------------------------------
|  Port  |   0   -  DV   |   1   |   2   -  D    |   3   -  D    |   4   |   5   |   6   |   7   |
--------------------------------------------------------------------------------------------------
| Cycles |  1.0     0.0  |  1.0  |  1.0     1.0  |  1.0     1.0  |  0.0  |  1.0  |  1.0  |  0.0  |
--------------------------------------------------------------------------------------------------

DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3)
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion occurred
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of   |                    Ports pressure in cycles                         |      |
|  Uops    |  0  - DV    |  1   |  2  -  D    |  3  -  D    |  4   |  5   |  6   |  7   |
-----------------------------------------------------------------------------------------
|   1*     |             |      |             |             |      |      |      |      | mov r9, rcx
|   2^     |             |      | 1.0     1.0 |             |      | 1.0  |      |      | cmp edx, dword ptr [r9+0x8]
|   0*F    |             |      |             |             |      |      |      |      | jnb 0x1e
|   1      |             |      |             |             |      |      | 1.0  |      | movsxd r10, edx
|   2      | 1.0         |      |             | 1.0     1.0 |      |      |      |      | add eax, dword ptr [r9+r10*4+0x10]
|   1      |             | 1.0  |             |             |      |      |      |      | inc edx
|   1*     |             |      |             |             |      |      |      |      | cmp edx, r8d
|   0*F    |             |      |             |             |      |      |      |      | jl 0xffffffffffffffea
Total Num Of Uops: 8

Таким образом, для ссылочного типа есть небольшое преимущество (1.72циклов на цикл против 1,74 циклов)

Я не эксперт в расшифровке выходных данных IACA, но я думаю, что это связано с использованием порта (лучше распределено для ссылочного типа между 2-3)

«Порт» - это микропроцессорные блоки в CPU.Например, для Skylake они делятся следующим образом (из таблиц инструкций от Agner оптимизируют ресурсы )

Port 0: Integer, f.p. and vector ALU, mul, div, branch
Port 1: Integer, f.p. and vector ALU
Port 2: Load
Port 3: Load
Port 4: Store
Port 5: Integer and vector ALU
Port 6: Integer ALU, branch
Port 7: Store address

Это выглядит как очень тонкая оптимизация микро-инструкций (моп), но можетОбъясните, почему.

Обратите внимание, что код кода для цикла можно улучшить следующим образом:

[Benchmark]
public int ValueTypeSum()
{
    var sum = 0;

    // NOTE: Caching the array to a local variable (that will avoid the reload of the Length inside the loop)
    var arr = _valueTypeData;
    // NOTE: checking against `array.Length` instead of `Size`, to completely remove the ArrayOutOfBound checks
    for (var i = 0; i < arr.Length; i++)
    {
        sum += arr[i].Value;
    }

    return sum;
}

Цикл будет немного лучше оптимизирован, и вы также должны иметь более согласованные результаты.

0 голосов
/ 10 декабря 2018

Я посмотрел на разборку в .NET Core 2.1 x64.

Код типа ref выглядит для меня оптимальным.Машинный код загружает каждую ссылку на объект, а затем загружает поле из каждого экземпляра.

Варианты типа значения имеют проверку диапазона массива.Петлевое клонирование не удалось.Эта проверка диапазона происходит потому, что верхняя граница цикла равна Size.Это должно быть array.Length, чтобы JIT мог распознать этот шаблон и не генерировать проверку диапазона.


Это ссылка на версию.Я пометил основной цикл.Хитрость для поиска основного цикла заключается в том, чтобы сначала найти обратный переход к вершине цикла.

enter image description here

Это вариант значения:

enter image description here

jae - это проверка диапазона.


Так что это ограничение JIT.Если вам не безразлично, откройте проблему GitHub в репозитории coreclr и скажите им, что клонирование цикла здесь не удалось.

Не унаследованный JIT в 4.7.2 имеет такое же поведение проверки диапазона.Сгенерированный код выглядит одинаково для версии ref:

enter image description here

Я не смотрел на устаревший код JIT, но я предполагаю, что его не удается устранитьлюбой диапазон проверок.Я считаю, что он не поддерживает клонирование петель.

0 голосов
/ 09 декабря 2018

Я думаю, что причиной такого близкого результата является использование такого маленького размера и отсутствие выделения чего-либо в куче (внутри цикла инициализации массива) для фрагментации элементов массива объектов.

В вашемтолько бенчмарк-код выделяет только элементы массива объектов из кучи (*), таким образом MemoryAllocator может размещать каждый элемент последовательно (**) в куче.Когда тестовый код начинает выполняться, данные будут считываться из кэшей ram в ЦП, и, поскольку элементы массива вашего объекта записываются в ram в последовательном (в непрерывном блоке) порядке, они будут кэшироваться, и поэтому вы не получаете никаких ошибок кэша.

Чтобы лучше это увидеть, у вас может быть другой массив объектов (желательно с большими объектами), который будет выделяться в куче для фрагментации элементов массива объектов, измеренных вами.Это может привести к тому, что пропадание кэша произойдет раньше, чем ваша текущая настройка.В реальном сценарии будут другие потоки, которые будут размещать в той же куче и дополнительно фрагментировать фактические объекты массива.Кроме того, доступ к оперативной памяти занимает гораздо больше времени, чем доступ к кэшу процессора (или циклу процессора).(Проверьте это сообщение относительно этой темы).

(*) Массив ValueType выделяет все пространство, необходимое для элементов массива, когда вы инициализируете его с помощью new ValueType[Size];Элементы массива ValueType будут смежными в ОЗУ.

(**) objectArr [i] элемент объекта и objectArr [i + 1] (и т. Д.) Будут располагаться рядом в куче, когда блок памяти будетв кеше, вероятно, все элементы массива объектов будут считаны в кэш процессора, поэтому при выполнении итерации по массиву доступ к оперативной памяти не потребуется.

...