Ответ 1
Прежде всего, анализ производительности на выходе компилятора -O0
обычно не очень интересен или полезен.
Правильно ли, что операция LEAL для вычисления правильного адреса намного быстрее, чем метод ADDL (+4)? Является ли это причиной разницы в производительности?
Нет, add
может выполняться на каждом рабочем порту ALU на любом процессоре x86. lea
обычно является низкой задержкой с простыми режимами адресации, но не такой хорошей пропускной способностью. В Atom он работает на другом этапе конвейера из обычных инструкций ALU, потому что он фактически соответствует его имени и использует AGU для этой микроархитектуры в порядке.
Смотрите x86 теги wiki для узнать, что делает код медленным или быстрым на разных микроархитектурах, особенно. Микроархитектура Agner Fog PDF и таблицы инструкций.
add
хуже, потому что он позволяет gcc -O0
сделать еще хуже код, используя его с местом назначения памяти, а затем загружая с него.
Компиляция с помощью -O0
даже не пытается использовать лучшие инструкции для задания. например вы получите mov $0, %eax
вместо xor %eax,%eax
, который вы всегда получаете в оптимизированном коде. Вы не должны делать вывод о том, что хорошо смотреть на не оптимизированный выход компилятора.
-O0
код всегда заполнен узкими местами, обычно при загрузке/хранении или хранении. К сожалению, IACA не учитывает задержку хранения в магазине, поэтому он не понимает, что эти петли фактически являются узким местом на
Насколько я знаю, как только новый адрес вычисляется до того, как память может быть указана, некоторые циклы часов должны пройти, поэтому второй цикл после добавления $4, -12 (% ebp) должен немного подождать, прежде чем продолжить,
Да, загрузка mov
-12(%ebp)
не будет готова примерно через 6 циклов после загрузки, которая была частью add
read-modify-write.
тогда как в первом цикле мы можем сразу ссылаться на память
Да
и в то же время LEAL будет вычислять следующий адрес
Нет.
Ваш анализ близок, но вы упустили тот факт, что следующая итерация еще должна загрузить значение, которое мы сохранили, в -12(%ebp)
. Таким образом, цепочка зависимостей, связанных с циклом, имеет одинаковую длину, и следующая итерация lea
не может начинаться раньше, чем в цикле, используя add
Проблемы с задержкой могут быть не узким местом пропускной способности:
Необходимо учитывать пропускную способность портаuop/выполнения. В этом случае тестирование OP показывает, что это действительно актуально. (Или латентность от конфликтов ресурсов.)
Когда gcc -O0
реализует ptr++
, он сохраняет старое значение в регистре, как вы сказали. Таким образом, адреса магазинов известны еще раньше, и там меньше нагрузки, требующей AGU.
Предполагая процессор Intel SnB-семейства:
## ptr++: 1st loop
movl -12(%ebp), %eax //1 uop (load)
leal 4(%eax), %edx //1 uop (ALU only)
movl %edx, -12(%ebp) //1 store-address, 1 store-data
// no load from -12(%ebp) into %eax
... rest the same.
## ++ptr: 2nd loop
addl $4, -12(%ebp) // read-modify-write: 2 fused-domain uops. 4 unfused: 1 load + 1 store-address + 1 store-data
movl -12(%ebp), %eax // load: 1 uop. ~6 cycle latency for %eax to be ready
... rest the same
Таким образом, часть указателя-приращения второго цикла имеет еще один загрузочный uop. Вероятно, узкие места кода на пропускной способности AGU (устройства формирования адресов). IACA говорит, что случай для arch = SNB, но это узкие места HSW на пропускной способности хранилища (не AGU).
Однако, не принимая во внимание задержку хранения в магазине, IACA говорит, что первый цикл может выполняться на одной итерации за 3,5 цикла, против одного на 4 цикла для второго цикла. Это быстрее, чем зависящая от цикла циклическая зависимость счетчика циклов addl $1, -48(%ebp)
, что указывает на то, что цикл ограничен узким интервалом с меньшей пропускной способностью AGU. (Конфликты ресурсов, вероятно, означают, что он на самом деле работает медленнее, чем одна итерация на 6c, см. Ниже).
Мы могли бы проверить эту теорию:
Добавление дополнительной загрузки uop к версии lea
, с критического пути, потребует большей пропускной способности, но не будет частью цепочек латентных циклов. например.
movl -12(%ebp), %eax //Get the current address
leal 4(%eax), %edx //Calculate the next address
movl %edx, -12(%ebp) //Store the new (next) address
mov -12(%ebp), %edx
%edx
вот-вот будет перезаписан mov
, поэтому нет никаких зависимостей от результата этой нагрузки. (Назначение mov
- только для записи, поэтому он разбивает цепи зависимостей, благодаря переименованию регистров.).
Таким образом, эта дополнительная загрузка приведет к тому, что цикл lea
достигнет такого же количества и вкуса, как и цикл add
, но с разной задержкой. Если дополнительная нагрузка не влияет на скорость, мы знаем, что первый цикл не является узким местом при загрузке/хранении.
Обновление: тестирование OP подтвердило, что дополнительная неиспользуемая загрузка замедляет цикл lea
до примерно той же скорости, что и цикл add
.
Почему возникают дополнительные проблемы, когда мы не сталкиваемся с узкими местами пропускной способности порта
uops запланированы в старом первом порядке (из uops, у которых есть их операнды), а не в порядке критического пути. Дополнительные сокеты, которые могли быть сделаны в свободном цикле позже, фактически задерживают uops, которые находятся на критическом пути (например, часть зависимой от цикла зависимостей). Это называется конфликтом ресурсов и может увеличить задержку критического пути.
то есть. вместо ожидания цикла, когда задержка критического пути оставила порт загрузки, и ничего не нужно делать, неиспользуемая нагрузка будет выполняться, когда будет загружена самая старая загрузка с ее загрузочным адресом. Это задержит другие нагрузки.
Аналогично, в цикле add
, где дополнительная загрузка является частью критического пути, дополнительная нагрузка вызывает больше конфликтов ресурсов, задерживая операции на критическом пути.
Другие догадки:
Итак, возможно, что адрес магазина уже готов к работе, это то, что он делает, поэтому операции с памятью лучше конвейерны. (например, проходы страницы TLB-miss могут начинаться раньше, когда приближаются к границе страницы. Даже обычная предварительная выборка аппаратного обеспечения не пересекает границы страниц, даже если они горячие в TLB. Цикл касается 4MiB памяти, чего достаточно для такого типа Дело в том, что L3-латентность достаточно высока, чтобы создать пузырь трубопровода. Или если ваш L3 мал, то основная память, безусловно, есть.
Или, может быть, дополнительная задержка просто усложняет выполнение вне порядка выполнения хорошей работы.