Зачем компилятор генерирует эту сборку? - программирование
Подтвердить что ты не робот

Зачем компилятор генерирует эту сборку?

При переходе через некоторый код Qt я столкнулся с следующим. Функция QMainWindowLayout::invalidate() имеет следующую реализацию:

void QMainWindowLayout::invalidate()
{
QLayout::invalidate()
minSize = szHint = QSize();
}

Он скомпилирован следующим образом:

<invalidate()>        push   %rbx
<invalidate()+1>      mov    %rdi,%rbx
<invalidate()+4>      callq  0x7ffff4fd9090 <QLayout::invalidate()>
<invalidate()+9>      movl   $0xffffffff,0x564(%rbx)
<invalidate()+19>     movl   $0xffffffff,0x568(%rbx)
<invalidate()+29>     mov    0x564(%rbx),%rax
<invalidate()+36>     mov    %rax,0x56c(%rbx)
<invalidate()+43>     pop    %rbx
<invalidate()+44>     retq

Сборка от invalidate + 9 до invalidate + 36 кажется глупой. Сначала код записывает -1 в% rbx + 0x564 и% rbx + 0x568, но затем он загружает это -1 из% rbx + 0x564 обратно в регистр, чтобы записать его в% rbx + 0x56c. Это похоже на то, что компилятор должен легко легко оптимизировать в один новый момент.

Итак, этот глупый код (и если да, то почему компилятор не оптимизирует его?) или это как-то очень умно и быстрее, чем просто немедленный переход?

(Примечание: этот код относится к обычной библиотеке релизов, отправленной ubuntu, поэтому она была скомпилирована GCC в режиме оптимизации. Переменные minSize и szHint являются нормальными переменными типа QSize.)

4b9b3361

Ответ 1

Не уверен, что ты прав, когда говоришь, что это глупо. Я думаю, что компилятор может попытаться оптимизировать размер кода здесь. Не существует 64-битной команды немедленного ввода памяти. Поэтому компилятор должен сгенерировать 2 команды mov так же, как и выше. Каждый из них будет составлять 10 байт, 2 сгенерированных движения - 14 байт. Это было написано так, что, скорее всего, нет латентности памяти, поэтому я не думаю, что вы принесете хоть какую-то производительность.

Ответ 2

Код "меньше совершенного".

Для размера кода эти 4 команды содержат до 34 байтов. Возможна намного меньшая последовательность (19 байт):

00000000  31C0              xor eax,eax
00000002  48F7D0            not rax
00000005  48898364050000    mov [rbx+0x564],rax
0000000C  4889836C050000    mov [rbx+0x56c],rax

;Note: XOR above clears RAX due to zero extension

Для производительности все не так просто. Процессор хочет делать много инструкций одновременно, и вышеприведенный код нарушает это. Например:

xor eax,eax
not rax                 ;Must wait until previous instruction finishes
mov [rbx+0x564],rax     ;Must wait until previous instruction finishes
mov [rbx+0x56c],rax     ;Must wait until "not" finishes

Для производительности вы хотите сделать это:

00000000  48C7C0FFFFFFFF        mov rax,0xffffffff
00000007  C78364050000FFFFFFFF  mov dword [rbx+0x564],0xffffffff
00000011  C78368050000FFFFFFFF  mov dword [rbx+0x568],0xffffffff
0000001B  C7836C050000FFFFFFFF  mov dword [rbx+0x56c],0xffffffff
00000025  C78370050000FFFFFFFF  mov dword [rbx+0x570],0xffffffff

;Note: first MOV sets RAX to 0xFFFFFFFFFFFFFFFF due to sign extension

Это позволяет выполнять все команды параллельно, без каких-либо зависимостей. К сожалению, он также намного больше (45 байт).

Если вы попытаетесь получить баланс между размером кода и производительностью; то вы можете надеяться, что первая инструкция (которая устанавливает значение в RAX) завершится до того, как последняя команда /s должна знать значение в RAX. Это может быть примерно так:

mov rax,-1
mov dword [rbx+0x564],0xffffffff
mov dword [rbx+0x568],0xffffffff
mov dword [rbx+0x56c],rax

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

Теперь; давайте посмотрим на исходный код и посмотрим, почему это плохо:

mov dword [rbx+0x564],0xffffffff
mov dword [rbx+0x568],0xffffffff
mov rax,[rbx+0x564]                ;Massive problem
mov [rbx+0x56C],rax                ;Depends on previous instruction

У современных процессоров есть что-то, называемое "пересылка хранилища", где записи хранятся в буфере, а будущие чтения могут получить значение из этого буфера, чтобы не считывать значение из кеша. По иронии, это работает только в том случае, если размер чтения меньше или равен размеру записи. "Пересылка магазина" не будет работать для этого кода, так как есть 2 записи, и чтение больше, чем оба. Это означает, что третья команда должна ждать, пока первые две команды не будут записаны в кеш, а затем должны прочитать значение из кеша; который может легко добавить к штрафу около 30 циклов и более. Затем четвертая команда должна ждать третьей инструкции (и не может произойти параллельно ни с чем), так что другая проблема.

Ответ 3

Я бы сломал строки как это (думаю, у нескольких есть те же самые комментарии)

Эти две строки взяты из встроенного определения QSize() http://qt.gitorious.org/qt/qt/blobs/4.7/src/corelib/tools/qsize.h которые устанавливают каждое поле отдельно. Кроме того, я предполагаю, что 0x564 (% rbx) является адресом szHint, который также устанавливается одновременно.

<invalidate()+9>      movl   $0xffffffff,0x564(%rbx)
<invalidate()+19>     movl   $0xffffffff,0x568(%rbx)

Эти строки, наконец, устанавливают minSize с использованием 64-битных операций, потому что компилятор теперь знает размер объекта QSize. И адрес minSize равен 0x56c (% rbx)

<invalidate()+29>     mov    0x564(%rbx),%rax
<invalidate()+36>     mov    %rax,0x56c(%rbx)

Примечание. Первая часть устанавливает два отдельных поля, а следующая часть копирует объект QSize (независимо от содержимого). Вопрос тогда в том, должен ли компилятор быть достаточно умным, чтобы построить составное 64-битное значение, потому что он видел предварительно установленные значения раньше? Не уверен в этом...

Ответ 4

В дополнение к ответу Гийома 64-разрядная загрузка/хранилище не выровнены. Но в соответствии с Руководство по оптимизации Intel (стр. 3-62)

Несогласованный доступ к данным может привести к существенным штрафам за производительность. Это особенно верно для разделов строки кэша. Размер кеша линия - 64 байта в Pentium 4 и других последних процессорах Intel, включая процессоры на базе микроархитектуры Intel Core.

Доступ к данным без выравнивания по 64-байтной границе приводит к двум операциям памяти доступа и требует выполнения нескольких μops (вместо одного). Доступ, который охватывает 64-байтные границы, скорее всего, приведет к большому штрафы за производительность, стоимость каждого стойла обычно выше машины с более длинными трубопроводами.

Из чего следует, что неуравновешенный load/store, который не пересекает границу строки кэша, является дешевым. В этом случае базовый указатель в процессе, который я отлаживал, был 0x10f9bb0, поэтому две переменные составляли 20 и 28 байтов в кэше.

Обычно процессоры Intel используют функцию store to load forwarding, поэтому загрузка только что сохраненного значения даже не нужно касаться кеша. Но тот же самый указатель также указывает, что большая нагрузка нескольких небольших магазинов не хранит-перегружает, а киоски: (p 3-66, p 3-68)

Правило сборки/компилятора. Правило 49. (H impact, M generality). Данные груз, который перенаправляется из магазина, должен быть полностью включен в пределах данных хранилища.

; A. Large load stall
mov     mem, eax        ; Store dword to address "MEM"
mov     mem + 4, ebx    ; Store dword to address "MEM + 4"
fld     mem             ; Load qword at address "MEM", stalls

Итак, этот код, вероятно, вызывает срыв, и поэтому я склонен считать, что он не оптимален. Я не был бы очень удивлен, если GCC полностью не учитывает такие ограничения. Кто-нибудь знает, если/сколько моделирования ограничений пересылки хранилища к загрузке GCC делает?

EDIT: некоторые экспериментируют с добавлением значений наполнителя до того, как поля minSize/szHint показывают, что GCC вообще не интересует границы границ кеша, и не делает clang.