Почему установка поля во много раз медленнее, чем получение поля?

Я уже знал, что установка поля намного медленнее, чем установка локальной переменной, но также указывает, что установка поля с локальная переменная выполняется намного медленнее, чем установка локальной переменной с полем. Почему это? В любом случае используется адрес поля.

public class Test
{
    public int A = 0;
    public int B = 4;

    public void Method1() // Set local with field
    {
        int a = A;

        for (int i = 0; i < 100; i++)
        {
            a += B;
        }

        A = a;
    }

    public void Method2() // Set field with local
    {
        int b = B;

        for (int i = 0; i < 100; i++)
        {
            A += b;
        }
    }
}

Результаты тестов с 10 и + 6 итерациями:

Method1: 28.1321 ms
Method2: 162.4528 ms

Ответ 1

Запустив это на моей машине, я получаю аналогичные разницы во времени, однако, глядя на JIT-код для 10-миллисекундных итераций, понятно, почему это так:

Метод A:

mov     r8,rcx
; "A" is loaded into eax
mov     eax,dword ptr [r8+8]
xor     edx,edx
; "B" is loaded into ecx
mov     ecx,dword ptr [r8+0Ch]
nop     dword ptr [rax]
loop_start:
; Partially unrolled loop, all additions done in registers
add     eax,ecx
add     eax,ecx
add     eax,ecx
add     eax,ecx
add     edx,4
cmp     edx,989680h
jl      loop_start
; Store the sum in eax back to "A"
mov     dword ptr [r8+8],eax
ret

И метод B:

; "B" is loaded into edx
mov     edx,dword ptr [rcx+0Ch]
xor     r8d,r8d
nop word ptr [rax+rax]
loop_start:
; Partially unrolled loop, but each iteration requires reading "A" from memory
; adding "B" to it, and then writing the new "A" back to memory.
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
mov     eax,dword ptr [rcx+8]
add     eax,edx
mov     dword ptr [rcx+8],eax
add     r8d,4
cmp     r8d,989680h
jl      loop_start
rep ret

Как видно из сборки, метод A будет значительно быстрее, поскольку значения A и B помещаются в регистры, и все дополнения происходят там, где промежуточная запись в память отсутствует. С другой стороны, метод B берет на себя нагрузку и хранит "A" в памяти для каждой отдельной итерации.

Ответ 2

В случае, если 1 a явно хранится в регистре. Все остальное было бы ужасным результатом компиляции.

Вероятно,.NET JIT не желает/не может конвертировать магазины в a для регистрации магазинов в случае 2.

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

Не удивительно, что .NET JIT выполняет небольшие оптимизации. Это хорошо известно последователям тега performance в переполнении стека.

По опыту я знаю, что JIT гораздо более склонна кэшировать нагрузки на память в регистрах. Поэтому случай 1 (по-видимому) не имеет доступа к B с каждой итерацией.

Регистровые вычисления дешевле, чем обращается к памяти. Это даже верно, если рассматриваемая память находится в кэше L1 процессора (как это имеет место здесь).

Я думал, что только локальные жители имеют право на кэширование процессора?

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

Ответ 3

method2: поле читается ~ 100x и задано ~ 100x too = 200x larg_0 (this) + 100x ldfld (поле загрузки) + 100x stfld (заданное поле) + 100x ldloc (local)

method1: поле считывается 100x, но не задано это эквивалентно методу1 минус 100x ldarg_0 (this)