Comments 24
Старый, добрый, ламповый Хабр! Спасибо за это чувство от прочтения статьи!
Автор обнаружил Immediate Folding, известную технологию, описанную в статьях и патентах как минимум с десяток раз. На данный момент она есть уже у всех, включая высокопроизволительные Arm микроархитектуры от Apple и Qualcomm.
Как решается вопрос с тем, что проверять результат суммирования надо после каждой суммы? Спекулятивное исполнение? Сложили всё сразу, а потом выполнили проверку для каждого результата?
out-of-order execution
register renaming
Ни то и ни то. Поищите хотя бы paper "continuous optimization" опубликован на isca 2005 ну и дальше от него по ссылкам. Там immediate folding оптимизация не глубоко расписана, но основное представление получить можно.
Или Reno: a rename based instruction optimizer. Там ещё более подробно.
При разворачивании цикла на две итерации второй inc rax можно заменить на dec rcx (фантомный, разумеется) Тогда можно заNOPать первый cmp / jb. Правда, в этом случае придётся делать отдельную копию rax, куда добавлять 2, а не 1.
Впрочем, вряд ли сделано именно так.
На языке высокого уровня тот же цикл можно записать так:
do { i++; } while(i < count); // inc rax; cmp rax, rcx; jb .loop
Не можно. Потому что применение такого цикла может иметь разную цель:
1. Задержка на какое-то число тактов
2. Выполнение какого-то действия число раз указанное в rcx
3. Извращенный способ загрузки в регистр rax значения равного значению в регистре rcx.
Соответственно полезный результат выполнения цикла может быть таким:
1. Прошло реальное время равное N тактов процессора
2. Некое действие выполнено X раз (к примеру сохранено X значений из порта ввода/вывода в оперативную память)
3. Значение rax стало равно значению rcx.
Возможная оптимизация в соответствии с целями:
1. Можно установить таймер на прерывание через время равное N.
2. Нельзя оптимизировать.
3. Тупо скопировать rcx в rax.
Однако при выполнении на Alder Lake i7-12700K мы получаем нечто совершенно иное: счётчик меток времени сообщает, что потрачено всего 391416518 тактов. 391416518 тактов, поделённые на 1073741824 итерации — это 0,364535 тактов на итерацию: в 2,7 раза быстрее, чем это возможно теоретически!
Это означает прежде всего то, что цикл с последовательными инкрементами регистра, это хреновый способ тестирования производительности современных процессоров.
И еще это какбэ намекает - не надо для организации задержек использовать цикл с инкрементом. Результат непредсказуем.
Если я не сделал серьёзной ошибки при сборе данных, что всегда возможно при таком тонком тестировании, то теперь у нас есть доказательство того, что ядро Golden Cove способно выполнять два последовательно зависимых инкремента за один такт.
Круто конечно. Но кому нужно делать два подряд инкремента одной переменой за один такт? Какой в этом сакральный смысл?
Конкретный способ обработки jb обычно не документируется, но в руководствам по микроархитектурам часто говорится, что он «совмещается» (fused) с cmp по крайней мере в части конвейера, то есть внутреннее представление cmp и jb движется через очереди как единое целое, а не обрабатывается как полностью отдельные микрооперации.
Чисто теоретически, можно предположить, что в микроархитектурах новых процессоров элементы ядра динамически конфигурируются по типу ПЛИС, и несколько команд могут объединяться в единый аппаратный логический блок, и выполняться за один такт. Последовательность "инкремент регистра, сравнение, условный переход" - как раз подходящий случай.
В частности, AnandTech использовал фразу «обрабатываются как NOP».
«обрабатываются как NOP» - в том смысле, что процессор на бегу оптимизирует поток команд, выбрасывая "бессмысленные"?
Угум-с, непонятно, какой именно "язык высокого уровня" автор имел в виду, но если C++, то по стандарту правильной интерпретацией является 3. Если бы автор взял, скажем, clang, то он бы зело удивился тому, что время выполнения вообще не зависит от разности i
и count
: https://godbolt.org/z/4M88EEq3x .
Я полагаю, автор привел цикл для примерной иллюстрации происходящего для людей, не понимающих язык ассемблера.
По поводу осмысленности остальных действий: я (как и автор) предполагал, что очень много read after write команд нельзя выполнить быстрее одного такта на команду. Он нашёл один тип процессора, который это может, и изучал как. Практическая применимость этого цикла вообще неважна, автор не предлагает использовать этот цикл для чего либо кроме анализа микроархитектуры процессора.
Я полагаю, автор привел цикл для примерной иллюстрации происходящего для людей, не понимающих язык ассемблера.
Для таких людей он разъяснил назначение каждой команды. - "Этот цикл состоит всего из трёх команд. Если воспользоваться терминологией языков высокого уровня, первая команда — inc rax — прибавляет к переменной единицу. Вторая — cmp rax, rcx — определяет, меньше ли переменная, чем желаемый счётчик итераций1. А третья — jb .loop — повторяет цикл, если меньше."
Пример на условном Си ничего к этому не добавляет, и дальше по тексту он не нужен. К тому же он некорректен, потому что результат компиляции может быть разным. Например цикл с i++ на Си может преобразоваться в "inc rax" в цикле, или в "mov rdx, 1" перед циклом и "add rax, rdx" в цикле. Оба варианта будут корректны относительно записи i++;
автор не предлагает использовать этот цикл для чего либо кроме анализа микроархитектуры процессора.
Изначально у него не было цели анализировать микроархитектуру. Автор измерял производительность используя разные бенчмарки, затем столкнулся с аномальной производительностью на Alder Lake при выполнении цикла с инкрементом, и попытался объяснить это особенностями архитектуры.
Загадку автор в итоге не разгадал, и предложил владельцам процессоров Alder Lake подключиться к исследованиям.
По словам автора, он не имеет возможности экспериментировать с процессорами Intel. - "Я пока не исследовал аномалию Kaby Lake, потому что, несмотря на множество попыток связаться с Intel, чтобы получить доступ к машине с Kaby Lake, мне не удалось договориться с ними. Я смог изучить рассмотренную в статье аномалию Alder Lake только потому, что один из проходящих курс студентов настроил для меня удалённое десктопное соединение со своей машиной."
Автор заметил интересный эффект, но достоверного объяснения ему не нашел. Предложил только гипотезы. Того кто сам пишет тесты, статья возможно вдохновит на эксперименты.
"еще это какбэ намекает - не надо для организации задержек использовать цикл с инкрементом."
Как бы всегда 1 отдельно взятое сложение было 1 цикл, это если взять 4 рядом то это 1 цикл. Или зависимые взять инструкции, 1 цикл на каждый add. Теперь надо + 1024 делать, чтобы разорвать 11 бит буфер внутри которого сложение занимает 0 латентности. Впрочем мануал Intel говорит что для этого надо использовать NOP разных типов.
Круто конечно. Но кому нужно делать два подряд инкремента одной переменой за один такт?
Можно делать ещё микроперации между этим. И вычитать тоже можно с 0 латентностью, если не выходить из буфера [-1024, 1023]. Add занимет 0 латентости, то есть можно засунуть инструкции туда между сложениями, и при этом всё равно сделать 5 сложений за цикл. Т.е. 5 инструкций запустить на выполнение и ещё 5 сложений или вычитаний.
Помню стажировался лет 15 назад в Intel Российском. Непонятно что вас удивляет.
1. То что команды выполняются параллельно? Ну да выполняются начиная с архитектуры Pentium. С тех пор количество "магии" и оптимизаций в микроархитектуре только выросло. Современные чипы CISC имеют внутри RISC-ядра и когда команды транслируются, они еще и оптимизируются. Независимые команды исполняются параллельно, несколько подряд идущих команд могут группироваться. Регистров в процессоре на самом деле сильно больше чем тех что вы видите в ассемблере. rax,rbx регистры - это лишь обозначение ассемблера, на самом деле их внутри сильно больше и процессор способен с каждым из них проводить операции независимо.
2. Каждый процессор имеет свои определенные особенности. Компилятор Intel и их библиотеки имеет оптимизированные версии распространенных функций для каждой микроархитектуры и особенностей кэша(!). Даже простое копирование будет по-разному выполнено на разных архитектурах.
3. Использовать VTune или аналогичный профайлер обязательно начиная с появления технологии Hyper Threading. Внезапно даже включая и выключая HT код может начать вести себя по-разному даже на одном и том же процессоре (потому что HT это неполноценная многоядерность).
Вот да, по описанию звучит так, что процессор научился в спекулятивное исполнение на уровне кремния. Это круто, конечно, и я похлопаю инженерам Интел, но зачем устраивать из этого загадку дыры?
На уровне понимания автор описал out-of-order && register renaming && score-boarding без употребления умных терминов, но по-сути. А дальше удивляется: они не дают наблюдаемого результата из-за DU-зависимости.
На этом фоне ваш комментарий похож на нагромождение умных слов без понимания сути под ними.
1. Автора удивляет разрыв DU-зависимости (единственной "истинной" зависимости в терминах компиляторов, которую разорвать действительно сложно, особенно в аппаратуре).
Причём разрыв DU-зависимости в аппаратуре.
2. Автор описал разрыв зависимости (и возможные решения) довольно качественно (что прямо супер для человека от железа\компиляторов далёкого).
3. Как указано выше - за такую технику отвечает Immediate Folding. Что весьма интересно.
Автор упустил всего одну вещь - наличие RISC ядра внутри процессоров Intel и наличие конвертации CISC команд в RISC. А следующее уже просто конкретные оптимизации конкретного процессора. После того как уперлись в физическую невозможность увеличивать частоту начали увеличивать размер кремния и фичи которые ответственны за трансляцию CISC->RISC. Уже много лет оптимизации строятся за счёт трасляции x86 команд в микроинструкции выполняемые внутри процессора. У интела каждые 4 года появляется новая микроархитектура с дополнительными улучшениями и удивительно что он это только недавно заметил. Судя по патенту прошло уже 2+ цикла, т.е. на рынке должно быть как минимум 4 поколения процессоров (разных по архитектуре и техпроцессу) где этот функционал реализован
Блин, какие длинные статьи сейчас пишутся... Насколько я понял, автор так и не узнал почему так происходит и выдал догадки? Ну так и я, ничего не зная о современных технологиях процов, предположу: допустим jb в связке с cmp не ориентируется на флаги (!), а видит, что rax далеко меньше rcx и точно знает что можно инкрементировать ещё много раз. Да, это слишком частный случай чтоб проц такое мог, но может Интел как-то хитро обобщили такую идею...
И объяснение тут от 3 января https://tavianator.com/2025/shlxplained.html
11 бит буфер — пока он не переполнится — можно add и sub с 0 латентностью, что значит Alder Lake даст ещё 6 uops исполнить за цикл.
запускается одновременно две версии цикла с +1 и +2 с возможностью оставить побочные эффекты только одно из них по итогам выполнения операции cmp
Слишком частный случай. А если между ними будут ещё операции?
проблемы с single core производительностью привели нас к тому, что современные камни жрут по 300 ватт и выжать пытаются лишний 0.1% прироста
одна старушка рубль, десять старушек -- червончик (с)
очевидно, что в камне есть супер-пупер продвинутая логика, которая как раз и умеет находить кейсы, корректные для спекулятивного выполнения
и вся эта статья, на самом деле, о том, что в 12-м поколении Intel ее, в очередной раз, сильно прокачала )
уже прокомментирвовали Immediate Folding - можно патент почитать: https://patents.google.com/patent/US20170123799A1/en
И да: это внесение некоторых "компиляторных техник" во фронтэнд CPU-конвейра.
И снова да: scope анализа в проце крайне ограничен (какой - не знаю).
И снова да как вам ответили: причина - выжимание максимума из single thread performance любыми средствами (например Intel гонит напряжение - что означает кубическое увеличение энергопотребление ради линейного увеличения тактовой частоты).
И объяснение тут от 3 января https://tavianator.com/2025/shlxplained.html
11 бит буфер — пока он не переполнится — можно add и sub с 0 латентностью, что значит Alder Lake даст ещё 6 uops исполнить за цикл.
Загадка потерянного инкремента