Подтвердить что ты не робот

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

Фон

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

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% издержки выполнения. Я должен задать вопрос:

Вопросы

  • Почему компилятор создает другой код сборки?

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

EDIT: На второй вопрос ответили предложение @KamyarSouri (j% 16). Теперь код сборки выглядит почти идентичным (см. http://pastebin.com/diff.php?i=yqXedtPm). Единственными отличиями являются линии 18, 33, 48:

000646F9  movzx       edx,dl 

Материал

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

enter image description here

gnuplot script для генерации графика: 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/анализ -

Параметры компоновщика: /INCREMENTAL: NO "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'" /SUBSYSTEM: CONSOLE/OPT: REF/OPT: ICF/LTCG/TLBID: 1/DYNAMICBASE/NXCOMPAT/MACHINE: X86/ERRORREPORT: QUEUE

4b9b3361

Ответ 1

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

Ваш массив 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% -ной разнице.

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


Пока я не инженер 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), преобразование не требуется. Поэтому проблему вообще можно избежать.

Ответ 2

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

Edit:

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

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

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

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