Зачем компилятор генерирует эту сборку?
При переходе через некоторый код 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
.)
Ответы
Ответ 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.