Нечетная проблема оптимизации при MSVC

Я видел этот блог:

http://igoro.com/archive/gallery-of-processor-cache-effects/

"странность" в части 7 - это то, что меня заинтересовало.

Моя первая мысль была "Thats просто С# быть странным".

Не я написал следующий код на С++.

volatile int* p = (volatile int*)_aligned_malloc( sizeof( int ) * 8, 64 );
memset( (void*)p, 0, sizeof( int ) * 8 );

double dStart   = t.GetTime();

for (int i = 0; i < 200000000; i++)
{
    //p[0]++;p[1]++;p[2]++;p[3]++;  // Option 1
    //p[0]++;p[2]++;p[4]++;p[6]++;  // Option 2
    p[0]++;p[2]++;                  // Option 3
}

double dTime    = t.GetTime() - dStart;

Время, которое я получаю на своем 2.4 ГГц Core 2 Quad, выглядит следующим образом:

Option 1 = ~8 cycles per loop.
Option 2 = ~4 cycles per loop.
Option 3 = ~6 cycles per loop.

Теперь это запутанно. Мое рассуждение о различии сводится к задержке записи кэша (3 цикла) на моем чипе и предположению, что кэш имеет 128-битный порт записи (это чистая работа над моей работой).

На этом основании в Варианте 1: он увеличит p [0] (1 цикл), затем увеличит p [2] (1 цикл), тогда он должен ждать 1 цикл (для кеша), затем p [1] (1 цикл), затем подождите 1 цикл (для кеша), затем p [3] (1 цикл). Наконец, 2 цикла для приращения и прыжка (хотя обычно он применяется как декремент и прыжок). Это дает всего 8 циклов.

В Варианте 2: он может увеличивать p [0] и p [4] за один цикл, а затем увеличивать p [2] и p [6] в другом цикле. Затем два цикла для вычитания и прыжка. В кеше нет необходимости. Всего 4 цикла.

В варианте 3: он может увеличивать p [0], тогда ему нужно подождать 2 цикла, а затем увеличить p [2], затем вычесть и перейти. Проблема в том, что если вы установили case 3 для увеличения p [0] и p [4], STILL принимает 6 циклов (что вызывает удаление моего 128-битного порта чтения/записи из воды).

Итак... может ли кто-нибудь сказать мне, что, черт возьми, здесь происходит? Почему дела 3 занимают больше времени? Также мне хотелось бы знать, в чем я ошибался в своем мышлении выше, поскольку у меня, очевидно, что-то не так! Любые идеи были бы высоко оценены!:)

Также было бы интересно посмотреть, как GCC или любой другой компилятор справляется с ним!

Редактировать: идея Джерри Коффина дала мне несколько мыслей.

Я сделал еще несколько тестов (на другой машине, так что простите изменение таймингов) с и без nops и с разным количеством nops

 case 2 - 0.46  00401ABD  jne         (401AB0h)

 0 nops - 0.68  00401AB7  jne         (401AB0h) 
 1 nop  - 0.61  00401AB8  jne         (401AB0h) 
 2 nops - 0.636 00401AB9  jne         (401AB0h) 
 3 nops - 0.632 00401ABA  jne         (401AB0h) 
 4 nops - 0.66  00401ABB  jne         (401AB0h) 
 5 nops - 0.52  00401ABC  jne         (401AB0h) 
 6 nops - 0.46  00401ABD  jne         (401AB0h) 
 7 nops - 0.46  00401ABE  jne         (401AB0h) 
 8 nops - 0.46  00401ABF  jne         (401AB0h)
 9 nops - 0.55  00401AC0  jne         (401AB0h) 

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

Итак, Джерри не прав (хотя его предложение немного помогает), однако что-то происходит. Я все больше и больше заинтригован, чтобы попытаться выяснить, что это такое. Похоже, что это скорее какая-то странность, а не какая-то странность.

Кто-нибудь хочет объяснить это любопытным умом?: D

Редактировать 3: Interjay имеет точку разматывания, которая удаляет предыдущее редактирование из воды. С развернутым контуром производительность не улучшается. Вам нужно добавить nop, чтобы сделать промежуток между источником перехода и пунктом назначения таким же, как и для моего хорошего nop-счета выше. Производительность все еще отстойная. Интересно, что мне нужно 6 ночей, чтобы улучшить производительность. Интересно, сколько процессоров может выдавать за цикл? Если его 3, то это объясняет задержку записи кэша... Но, если это так, почему возникает латентность?

Любопытный и любопытный...

Ответы

Ответ 1

Хорошо, у меня была короткая беседа с инженером-интеллектуалом именно по этой проблеме и получил этот ответ:

Это явно связано с тем, какие инструкции заканчиваются тем, что исполнительные устройства, как быстро машина обнаруживает нагрузку на магазин проблема, и как быстро и элегантно это касается разворачивания спекулятивное выполнение, чтобы справиться с ним (или если оно принимает несколько циклов из-за некоторого внутреннего конфликта). Но это сказало - вам нужно очень подробный pipetrace и симулятор, чтобы понять это. Предсказание обработка инструкций вне порядка в этих конвейерах слишком сложна делать на бумаге даже для людей, которые проектировали машины. Для миряне - нет надежды в аду. Извините!

Ao Я думал, что добавлю ответ здесь и закрою этот вопрос раз и навсегда:)

Ответ 2

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

Ответ 3

Это не похоже на компилятор. Сначала я думал, что это может быть связано с трюками компилятора, такими как разворот цикла, но, глядя на сгенерированную сборку, MSVC 9.0 просто генерирует простой перевод из кода на С++.

Вариант 1:

[email protected]:
    add DWORD PTR [esi], ecx
    add DWORD PTR [esi+4], ecx
    add DWORD PTR [esi+8], ecx
    add DWORD PTR [esi+12], ecx
    sub eax, ecx
    jne SHORT [email protected]

Вариант 2:

[email protected]:
    add DWORD PTR [esi], ecx
    add DWORD PTR [esi+8], ecx
    add DWORD PTR [esi+16], ecx
    add DWORD PTR [esi+24], ecx
    sub eax, ecx
    jne SHORT [email protected]

Вариант 3:

[email protected]:
    add DWORD PTR [esi], ecx
    add DWORD PTR [esi+8], ecx
    sub eax, ecx
    jne SHORT [email protected]

Ответ 4

Набор инструкций x86 никоим образом не является репрезентативным для того, что действительно делает процессор. Инструкции переведены на внутренний машинный язык, термин "микрооперация" был введен в течение 486 дней. Бросьте такие вещи, как переименование регистров, спекулятивное исполнение, множественные исполнительные блоки и их взаимодействие с кешем, и просто невозможно предсказать, как долго что-то нужно предпринять. Производители чипов давно прекратили публикацию прогнозов времени цикла. Их проекты являются коммерческой тайной.