Почему встраивание компилятора приводит к более медленному коду, чем встраивание вручную? - PullRequest
31 голосов
/ 21 декабря 2011

Фон

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

for(int j=n;--j>0;)
    asd[j%16]=a.e<b.e;

a и b относятся к классу ASD:

struct ASD  {
    float e;
    ...
};

Я исследовал эффект от помещения этого сравнения в функцию облегченного члена:

bool test(const ASD& y)const {
    return e<y.e;
}

и использовать его так:

for(int j=n;--j>0;)
    asd[j%16]=a.test(b);

Компилятор встроил эту функцию, но проблема в том, что код сборки будет другим и будет вызывать> 10% накладных расходов времени выполнения. У меня вопрос:

Вопросы

  1. Почему компилятор выдает другой код сборки?

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

РЕДАКТИРОВАТЬ: На второй вопрос был дан ответ путем реализации предложения @ KamyarSouri (j% 16). Код ассемблера теперь выглядит практически идентично (см. http://pastebin.com/diff.php?i=yqXedtPm).. Разница только в строках 18, 33, 48:

000646F9  movzx       edx,dl 

Материал

Эта диаграмма показывает FLOP / с (до коэффициента масштабирования) для 50 тестовых прогонов моего кода.

enter image description here

Скрипт gnuplot для генерации сюжета: http://pastebin.com/8amNqya7

Параметры компилятора:

/ Zi / W3 / WX- / MP / Ox / Ob2 / Oi / Ot / Oy / GL / D "WIN32" / D "NDEBUG" / D "_CONSOLE" / D "_UNICODE" / D "UNICODE" / Gm- / EHsc / MT / GS- / Gy / arch: SSE2 / fp: точный / Zc: wchar_t / Zc: forScope / Gd / analysis-

Параметры компоновщика: / INCREMENTAL: НЕТ "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" " uuid.lib "" odbc32.lib "" odbccp32.lib "/ ALLOWISOLATION / MANIFESTUAC:" level = 'asInvoker' uiAccess = 'false' "/ ПОДРАЗДЕЛЕНИЕ: КОНСОЛЬ / ОПТ: REF / OPT: ICF / LTCG / TLBID: 1 / DYNAMICBASE / NXCOMPAT / MACHINE: X86 / ERRORREPORT: QUEUE

Ответы [ 2 ]

31 голосов
/ 21 декабря 2011

Краткий ответ:

Ваш массив asd объявлен так:

int *asd=new int[16];

Поэтому используйте int в качестве типа возврата, а не bool.
Или измените тип массива на bool.

В любом случае, чтобы тип возвращаемого значения функции test соответствовал типу массива.

Для получения более подробной информации перейдите к основанию.

Длинный ответ:

В версии, встроенной вручную, «ядро» одной итерации выглядит так:

xor         eax,eax  

mov         edx,ecx  
and         edx,0Fh  
mov         dword ptr [ebp+edx*4],eax  
mov         eax,dword ptr [esp+1Ch]  
movss       xmm0,dword ptr [eax]  
movss       xmm1,dword ptr [edi]  
cvtps2pd    xmm0,xmm0  
cvtps2pd    xmm1,xmm1  
comisd      xmm1,xmm0  

Версия, встроенная в компилятор, полностью идентична, за исключением первой инструкции.

Где вместо:

xor         eax,eax

имеет:

xor         eax,eax  
movzx       edx,al

Хорошо, значит, одна дополнительная инструкция. Они оба делают то же самое - обнуляют регистр. Это единственное отличие, которое я вижу ...

Инструкция movzx имеет задержку одного цикла и обратную пропускную способность цикла 0.33 на всех более новых архитектурах. Так что я не могу представить, как это могло бы сделать разницу в 10%.

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


Хотя я не инженер Intel, вот мое предположение:

Большинство современных процессоров имеют дело с операциями обнуления (такими как xor eax,eax) через переименование регистров в банк нулевых регистров. Он полностью обходит исполнительные блоки. Однако, возможно, что эта специальная обработка могла вызвать пузырь конвейера, когда к (частичному) регистру обращаются через movzx edi,al.

Кроме того, есть также ложная зависимость от eax в встроенной версии компилятора:

movzx       edx,al  
mov         eax,ecx  //  False dependency on "eax".

Способен ли решить проблему внеочередное выполнение , это мне не под силу.


Хорошо, это в основном превращается в вопрос реверс-инжиниринга компилятора MSVC ...

Здесь я объясню почему , что генерируется дополнительный movzx, а также почему он остается.

Ключом здесь является bool возвращаемое значение. По-видимому, bool типы данных, вероятно, представляют собой хранимые 8-битные значения внутри внутреннего представления MSVC. Поэтому, когда вы неявно конвертируете из bool в int здесь:

asd[j%16] = a.test(b);
^^^^^^^^^   ^^^^^^^^^
 type int   type bool

есть 8-разрядное -> 32-разрядное целочисленное продвижение. По этой причине MSVC генерирует инструкцию movzx.

Когда встраивание выполняется вручную, компилятор имеет достаточно информации для оптимизации этого преобразования и сохраняет все как 32-битный тип данных IR.

Однако, когда код помещается в свою собственную функцию с возвращаемым значением bool, компилятор не может оптимизировать 8-битный промежуточный тип данных. Следовательно, movzx остается.

Когда вы делаете оба типа данных одинаковыми (int или bool), преобразование не требуется. Следовательно, проблема полностью исключена.

1 голос
/ 21 декабря 2011

lea esp,[esp] занимает 7 байтов i-кеша и находится внутри цикла.Из нескольких других подсказок видно, что компилятор не уверен, является ли это сборкой релиза или сборкой отладки.

Редактировать:

lea esp,[esp] не в цикле.Положение среди окружающих инструкций ввело меня в заблуждение.Теперь это выглядит так, как будто намеренно потрачено впустую 7 байтов, а затем еще 2 потраченных впустую байта, чтобы начать реальный цикл с 16-байтовой границы.Это означает, что это на самом деле ускоряет процесс, как заметил Йоэннес Герер.

Компилятор все еще не уверен, является ли это отладочной или релизной сборкой.

Другое редактирование:

Различие в pastebin отличается от различий pastebin, которое я видел ранее.Этот ответ сейчас можно удалить, но в нем уже есть комментарии, поэтому я его оставлю.

...