задержки и измерение конкретных инструкций

Поскольку современный процессор использует тяжелый конвейер даже для ALU, несколько выполнений независимых арифметических операций могут выполняться за один цикл, например, четыре операции добавления могут выполняться за 4 цикла, а не 4 * задержки одного добавления.

Даже при наличии конвейеров и наличии конфликтов на портах выполнения я хотел бы реализовать задержки с точностью до цикла, выполняя некоторые инструкции таким образом, чтобы время выполнения последовательности инструкций было предсказуемым. Например, если инструкция x занимает 2 цикла и не может быть обработана конвейером, то, выполняя x четыре раза, я ожидаю, что смогу установить задержки на 8 циклов.

Я знаю, что это обычно невозможно для пользовательского пространства, потому что ядро ​​может вмешиваться между последовательностями выполнения и может привести к большей задержке, чем ожидалось. Однако я предполагаю, что этот код выполняется на стороне ядра без прерываний или изолированного ядра, свободного от шума.

Взглянув на https://agner.org/optimize/instruction_tables.pdf, я обнаружил, что инструкция CDQ не требует операции с памятью и занимает 1 цикл в своей задержке и обратной пропускной способности. Если я правильно понимаю, это означает, что если нет конкуренции за порт, используемый CDQ, он может выполнять эту инструкцию в каждом цикле. Чтобы проверить это, я помещаю CDQ между таймером RDTSC и устанавливаю частоту ядра как номинальную частоту ядра (в надежде, что она такая же, как цикл TSC). Также я прикрепил два процесса к гиперпоточным ядрам; один попадает в цикл while (1), а другой выполняет инструкцию CDQ. Кажется, что добавление одной инструкции увеличивает 1-2 цикла TSC.

Однако меня беспокоит случай, когда требуется много инструкций CDQ для установки больших задержек, таких как 10000, что может потребовать как минимум 5000 инструкций. Если размер кода слишком велик для размещения в кеше инструкций и вызывает промах в кеше и промах TLB, это может вызвать некоторые колебания в моей задержке. Я попытался использовать простой цикл for для выполнения инструкций CDQ, но не могу гарантировать, можно ли использовать цикл for (реализованный с помощью jnz, cmp и sub), потому что он также может внести некоторый неожиданный шум в мою задержку. Может ли кто-нибудь подтвердить, могу ли я использовать инструкцию CDQ таким образом?

Добавлен вопрос

После тестирования с несколькими инструкциями CMC кажется, что 10 инструкций CMC добавляют 10 циклов TSC. Я использовал приведенный ниже код для измерения времени выполнения 0, 10, 20, 30, 40, 50

    asm volatile(                                                                                                                                                                                                                                                                               
        "lfence\t\n"                                                                                                                                                                                                                                                                            
        "rdtsc\t\n"                                                                                                                                                                                                                                                                             
        "lfence\t\n"                                                                                                                                                                                                                                                                            
        "mov %%eax, %%esi\t\n"
                                                                                                                                                                                                                                                                                                
        "cmc\n\t" // CMC * 10, 20, 30, 40, ...
                                                                                                                                                                                                                                                                                                
        "rdtscp\n\t"                                                                                                                                                                                                                                                                            
        "lfence\t\n"                                                                                                                                                                                                                                                                            
        "sub %%esi, %%eax\t\n"
        :"=a"(*res)
        :
        : "ecx","edx","esi", "r11"
    );

    printf("elapsed time:%d\n", *res);

Я получил 44-46, 50-52, 62-64, 70-72, 80-82, 90-92 для (без CMC, 10CMC, 20CMC, 30CMC, 40CMC, 50CMC). Когда результаты RDTSC варьируются от 0 до 2 циклов TSC при каждом выполнении, кажется, что инструкция 1CMC соответствует задержке в 1 цикл. За исключением первого раза добавления 10 CMC (он увеличивается не на 10, а на 6 ~ 8), большую часть времени добавление еще 10 инструкций CMC добавляет (10 + -2) больше цилиндров TSC. Однако, когда я изменил CMC на инструкцию CDQ, как я изначально использовал в вопросе, кажется, что 1 инструкция CDQ не соответствует 1 циклу в машине i9900K. Однако, когда я смотрю на таблицу оптимизации agner, кажется, что инструкции CMC и CDQ на самом деле не отличаются. Связано ли это с тем, что инструкции CMC друг за другом не зависят друг от друга, а инструкции CDQ имеют зависимость между ними?

Также, если мы считаем, что переменная задержка была вызвана rdtsc, а не из-за прерывания или других проблем конкуренции ... тогда кажется, что инструкция CMC может использоваться для задержки 1 основного цикла, верно? Потому что я привязал свое ядро ​​к работе с тактовой частотой 3,6 ГГц, которая предполагалась тактовой частотой TSC на i9900k .. Я просмотрел упомянутый вопрос, но не могу уловить точных деталей ..


person ruach    schedule 22.09.2020    source источник
comment
За исключением микросхем, таких как микроконтроллеры PIC не-mips, и других с аналогичными ситуациями, вы не можете сделать это за пределами, скажем, тестовой среды. Конечно, с контролируемой выборкой или идеально повторяемым исполнением, где вы можете настроить его вручную, но в реальном мире даже с одной материнской платой, но, конечно, во всем мире x86, невозможно сделать это последовательно. Не то, чтобы тратить время на попытки. Вам нужно точное время, вы используете для этого периферийное устройство, предназначенное для того, чтобы делать то, что вы хотите делать.   -  person old_timer    schedule 22.09.2020
comment
В программном обеспечении вы часто можете делать это, по крайней мере, так долго, я могу сделать это не меньше, чем это количество времени, но это могло бы быть намного дольше. который полезен для работы с различными периферийными устройствами или другими вещами, но не для точного измерения времени.   -  person old_timer    schedule 22.09.2020
comment
Затем добавляется операционная система, и все становится еще хуже.   -  person old_timer    schedule 22.09.2020
comment
cdq в Zen 2 имеет пропускную способность 4 за такт, по сравнению с явно 1 за такт на Skylake (измерено), хотя вы ожидаете, что он будет работать 2 за такт, исходя из возможности работать на порте 0 или порту 6. Может, он имеет выходную зависимость от Intel? В любом случае, это тактовые циклы ядра, а не фиксированная частота. А выполнение вне очереди означает, что эти задержки имеют значение только в течение длинных интервалов, значительно превышающих размер ROB (буфер переупорядочения), 224 мопов на Skylake или по крайней мере 97 мопов RS. В этот момент он достаточно длинный, чтобы прерывание могло вызвать гораздо более длительную задержку.   -  person Peter Cordes    schedule 22.09.2020
comment
Если по какой-то причине вы ищете однобайтовую инструкцию, которая определенно имеет истинную зависимость от самой себя (ограничение выполнения узким местом задержки 1 / такт), cmc (переключение CF) может помочь. Но это вряд ли полезно для вашей общей цели цикла задержки. Возможно, в сочетании с lfence, но это может задержать намного дольше, чем вы хотите, в зависимости от того, сколько времени займет выполнение существующих инструкций в полете. например загрузка из-за промаха кэша.   -  person Peter Cordes    schedule 22.09.2020
comment
Я не думаю, что это точный дубликат в том смысле, что другой вопрос фокусируется на более длительных временах задержки, измеряемых в реальном времени (так, например, DVFS вызывает проблему), в то время как этот, кажется, ориентирован на точные для цикла задержки, измеряемые в циклах.   -  person BeeOnRope    schedule 22.09.2020
comment
Это очень похоже на проблему XY. Вы можете объяснить, для чего бы вы хотели использовать такую ​​задержку? Кто-то, вероятно, предложит лучший способ достижения вашей основной цели.   -  person Nate Eldredge    schedule 22.09.2020
comment
Спасибо всем за комментарии. Что я хочу сделать, так это поставить задержки Xsecons с задержками на уровне измеряемых циклов TSC, которые должны быть очень маленькими. Причина, по которой мне нужна такая задержка, заключается в том, что я хотел бы выполнить свои инструкции между двумя инструкциями на гиперпоточном ядре. Я мог бы сделать свое ядро ​​полностью изолированным от шума, вызванного ядром (прерыванием). Я запускал этот код на машине coffelake, и кажется, что эта инструкция может быть выполнена на двух портах (либо p0, либо p6), и несколько CDQ не могут быть конвейеризованы (предсказано из Reciprocal throughput 1), поэтому я ожидал, что   -  person ruach    schedule 23.09.2020
comment
добавление одного CDQ задерживает мое ядро ​​на один основной цикл. Также, чтобы проверить, действительно ли это задерживает цикл моего ядра 1, я использовал CPUPOWER, чтобы закрепить мои ядра для работы на номинальной частоте 3,6 ГГц в Coffelake. Несмотря на наличие CPUPOWER, он не может работать точно на 3,6 ГГц при каждом выполнении, поэтому может вносить некоторые шумы при измерении TSC. И на самом деле, у меня есть переменная задержка для выполнения N-го числа CDQ (я правильно поставил все между двумя rdtsc с lfecne & c).   -  person ruach    schedule 23.09.2020
comment
В любом случае, я понимаю, что даже несмотря на то, что все еще может быть шумно ожидать, что он прошел N циклов после выполнения N инструкций CDQ, я хотел бы знать, можно ли ввести N + - некоторую задержку шума на моем ядре в моих настройках. Что я действительно хочу сделать, так это не измерять время, чтобы проверить, прошло ли N циклов. Я просто хочу установить задержки с точностью до N циклов.   -  person ruach    schedule 23.09.2020
comment
@PeterCordes Если вы не возражаете, не могли бы вы взглянуть на добавленный вопрос? Я ценю вашу помощь.   -  person ruach    schedule 23.09.2020
comment
Типичный 80x86 работает примерно на 10% от номинальной частоты (например, при пониженной частоте из-за достижения теплового предела и совместного использования ядра с другим логическим процессором) до 150% от номинальной частоты (например, целое ядро ​​на себя и получение турбо-ускорения). Имея это в виду, задержка с точностью до цикла на самом деле является случайной задержкой без точности, которая не имеет никакого смысла для каких-либо практических целей.   -  person Brendan    schedule 23.09.2020
comment
@Brendan Спасибо за комментарий. Я знаю, что при каждом выполнении может быть джиттер, и довольно сложно ввести фиксированную задержку уровня цикла, но кажется, что я могу добавить очень мелкую задержку с мыслью о нескольких инструкциях cmc. Не могли бы вы поделиться каким-либо мнением по этому поводу? Я ценю это!   -  person ruach    schedule 23.09.2020
comment
@JaehyukLee: Для очень маленьких и точных задержек единственный вариант - это сдаться, а затем заново оценить причину, по которой вы сделали ошибку, подумав, что вы этого хотите. Для кода ядра; для более длительных задержек с немного меньшей точностью вы можете изучить использование локального таймера APIC в режиме крайнего срока TSC (возможно, с некоторой корректировкой времени выхода IRQ) и / или аналогичным образом со счетчиками мониторинга производительности.   -  person Brendan    schedule 23.09.2020
comment
@JaehyukLee: Хм, я бы хотел выполнить свои инструкции между двумя инструкциями на гиперпоточном ядре ... Это действительно возможно (если вы забудете о задержках и забудете о двух инструкциях), но вам нужно будет построить что-то вроде диаграмма Ганта (с конвейерами в виде строк), описывающая несколько инструкций в полете одновременно и использующая умное злоупотребление конкуренцией для конвейеров как средство синхронизации потоков инструкций в начале.   -  person Brendan    schedule 23.09.2020
comment
@Brendan хм ... звучит интересно, но я не знаю, как точно реализовать эту ситуацию ... не могли бы вы поделиться каким-нибудь примером, если вы не возражаете. В настоящее время я хотел бы выполнить инструкцию загрузки, которая попадет в кеш l1 между двумя инструкциями загрузки на другом гиперпоточном ядре, и они попадут в кеш l1 также потому, что я предварительно загрузил их перед выполнением.   -  person ruach    schedule 23.09.2020
comment
@JaehyukLee: Если в ядре 8 конвейеров (от P0 до P7); и если один ЦП выполняет длинные инструкции, которые используют определенные конвейеры в определенном порядке (P0, затем P1, затем P2 ..), а другой ЦП выполняет более короткие инструкции, которые используют конвейеры в том же порядке; затем в конечном итоге (после повторения последовательности несколько раз) ЦП, выполняющий более короткие инструкции, начнет останавливаться до тех пор, пока конвейер / ы не освободятся, в результате чего оба ЦП будут синхронизированы друг с другом. Я понятия не имею, возможно ли это на практике.   -  person Brendan    schedule 23.09.2020
comment
@JaehyukLee: Если вы можете использовать это, чтобы установить синхронизацию между процессорами; тогда вы могли бы (с многодневной предельной осторожностью) также составить последовательности инструкций, которые сохранят синхронизацию в такт после того, как она будет установлена. Конечно, есть много вещей (IRQ, промахи в кеше, исправление ошибок ECC, неверные предсказания ветвлений и т. Д.), Которые могут нарушить синхронизацию после и смягчить то, что вам, вероятно, потребуется принудительно все. (Все кеши, цель / направление ветвления буферы, TLB, ...) в известное состояние, прежде чем пытаться синхронизировать и периодически повторять его.   -  person Brendan    schedule 23.09.2020
comment
@JaehyukLee: у вас все наоборот: CMC действительно зависит от предыдущего CMC, потому что он считывает и записывает флаг переноса. CDQ этого не делает: он читает EAX и записывает EDX. Если вы тестируете его в цикле (с синхронизацией вне цикла), он запускает 2 CDQ / цикл, узкое место на портах на Skylake.   -  person Peter Cordes    schedule 23.09.2020
comment
Я не знаю, почему Агнер измерил пропускную способность 1 с для CDQ на Coffee Lake, а 0,5 с для CQO, но его таблица обновляется вручную, поэтому ошибки случаются. uops.info неожиданно измерил пропускную способность 1,0 цикла на Skylake-X, но пропускную способность 0,6 цикла на Skylake (клиент), что я ' м, тестирование на. Я измеряю 0,5, включая одну взятую ветвь, которая также конкурирует за порт 6, используя цикл типа times 10 cdq / dec ebp / jnz в статическом исполняемом файле, который я синхронизировал с помощью счетчиков производительности HW, поэтому мне не приходилось полагаться на RDTSC.   -  person Peter Cordes    schedule 23.09.2020


Ответы (1)


У вас есть 4 основных варианта:

  • отложите вторую операцию, задав ей зависимость данных от (результата) первой.
  • lfence, последовательность с фиксированной задержкой, lfence. Оба они могут дать только минимальную задержку; может быть намного больше в зависимости от масштабирования частоты процессора и / или прерываний.
  • вращать на rdtsc до крайнего срока (который вы каким-то образом рассчитываете, например, на основе более раннего rdtsc) или более продолжительный сон на основе крайнего срока TSC, например. используя локальный APIC.
  • Откажитесь и используйте другую конструкцию или используйте исправный микроконтроллер, где вы можете получить надежную синхронизацию с точностью до цикла при фиксированной тактовой частоте.

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

Если вам нужны точные задержки всего в несколько тактов, вы в основном облажались; суперскалярное выполнение вне очереди, прерывания и переменная тактовая частота делают это практически невозможным в общем случае. Как объяснил @Brendan:

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

Для кода ядра; для более длительных задержек с немного меньшей точностью вы можете изучить использование локального таймера APIC в режиме крайнего срока TSC (возможно, с некоторой корректировкой времени выхода IRQ) и / или аналогичным образом со счетчиками мониторинга производительности.

Для задержек в несколько десятков тактовых циклов подождите, пока RDTSC не получит искомое значение. Как рассчитать время задержки asm цикл на x86 linux? Но у него есть минимальные накладные расходы на выполнение RDTSC дважды или RDTSC плюс TPAUSE, если у вас есть расширение waitpkg ISA. (У вас нет на i9-9900k). Вам также понадобится lfence, если вы хотите полностью остановить выполнение не по порядку.

Если вам нужно что-то делать каждые 20 нс или что-то в этом роде, увеличивайте крайний срок вместо того, чтобы пытаться делать фиксированную задержку между другими работами. Таким образом, изменение другой работы не приведет к накоплению ошибок. Но одно прерывание отбросит вас далеко вперед и приведет к тому, что вы будете выполнять другую работу подряд, пока вы не наверстаете упущенное. Таким образом, помимо проверки срока, вы также можете проверить, не сильно ли он отстает от срока, и взять новый образец TSC.

(TSC тикает с постоянной частотой на современной x86, но частота ядра не работает: см. Как получить количество циклов ЦП в x86_64 из C ++? для получения дополнительной информации)


Может быть, вы можете использовать зависимость данных между вашей реальной работой?

Небольшие задержки в несколько тактов, меньшие, чем размер неработающего планировщика 1, на самом деле невозможны без принятия во внимание окружающего кода и знания точной микроархитектуры. вы выполняете.

сноска 1: 97: запись RS для созданных в Skylake uarches, хотя есть некоторые свидетельства того, что это не совсем унифицированный планировщик: некоторые записи могут содержать только некоторые виды uops.

Если вы можете создать зависимость данных между двумя объектами, которые вы пытаетесь разделить, вы сможете таким образом создать минимальную задержку между их выполнением. Есть способы связать цепочку зависимостей с другой. регистр, не влияя на его значение, например and eax, 0 / or ecx, eax заставляет ECX зависеть от инструкции, написавшей EAX, не влияя на значение ECX. (Сделать регистр зависимым от другого без изменения его значение).

например между двумя загрузками вы можете создать зависимость данных от результата загрузки одного в адрес загрузки более поздней загрузки или в адрес магазина. Связывание двух адресов магазинов вместе с цепочкой зависимостей не так хорошо; первое хранилище может занять кучу дополнительного времени (например, из-за промаха dTLB) после того, как адрес станет известен, поэтому в конечном итоге два хранилища в конечном итоге совершают повторную фиксацию. Вам может понадобиться mfence, а затем lfence между двумя магазинами, если вы хотите установить задержку перед вторым магазином. См. Также Загружаются и сохраняются единственные инструкции, которые переупорядочиваются. ?, чтобы узнать больше о OoO exec в lfence (и mfence на Skylake).

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


CMC - одна из немногих однобайтовых инструкций, доступных в 64-битном режиме, которую можно просто повторить, чтобы создать узкое место задержки (1 цикл на инструкцию на большинстве процессоров) без доступа к памяти (например, lodsb который узкие места при слиянии в младший байт RAX). xchg eax, reg тоже подойдет, но на Intel это 3 мупа.

Вместо lfence вы можете связать эту цепочку dep с конкретной инструкцией, используя adc reg, 0, если вы начнете с известного состояния CF и используете нечетное или четное количество инструкций CMC, так что CF = 0 в этой точке. Или cmovc same,same сделает значение регистра зависимым от CF без его изменения, независимо от того, был ли CF установлен или очищен.

Однако однобайтовые инструкции могут создавать странные интерфейсные эффекты, когда у вас слишком много строк подряд, которые кэш uop не может обработать. Вот что замедляет CDQ, если вы повторяете это бесконечно; очевидно, что Skylake может декодировать его только с частотой 1 / такт в устаревших декодерах. Может ли простой декодеры в последних микроархитектурах Intel обрабатывают все инструкции с 1-ой операцией?. Это может быть нормально и / или то, что вы хотите. 3 цикла на 3-байтовую инструкцию позволяют кэшировать этот код в кеш-памяти uop, например imul eax, eax или imul eax, 0. Но, возможно, лучше избегать загрязнения кеш-памяти uop кодом, который должен работать медленно.

Между инструкциями LFENCE cld составляет 3 мопа и имеет пропускную способность 4c на Skylake, поэтому, если вы используете lfence в начале / конце вашей задержки, это можно использовать.


Кроме того, конечно, любая задержка точного счисления с точки зрения определенного количества некоторых инструкций (не rdtsc) будет зависеть от тактовой частоты ядра, не эталонной частоты. И в лучшем случае это минимальная задержка; если во время цикла задержки возникает прерывание, общая задержка будет близка к общему времени обработки прерывания плюс все, что заняло цикл задержки.

Или, если ЦП работает на холостом ходу (часто 800 МГц), задержка в наносекундах будет намного больше, чем если бы ЦП работал в режиме максимального турбо.


Re: ваш 2-й эксперимент с CMC между барьерами lfence OoO exec

Да, вы можете довольно точно контролировать тактовые циклы ядра между двумя инструкциями lfence или между lfence и rdtscp с помощью простой цепочки зависимостей, инструкции pause или узкого места пропускной способности на некоторых исполнительных модулях, возможно, целочисленном или делителе FP. Но я предполагаю, что в вашем реальном варианте использования важна общая задержка между вещами перед первым lfence и прочими вещами после 2-го lfence.

Первый lfence должен дождаться тех инструкций, которые ранее находились в полете, чтобы удалиться из вышедшего из строя бэкенда (ROB = буфер переупорядочения, 224 мопы слияния домена в семействе Skylake). Если они включали какие-либо нагрузки, которые могут отсутствовать в кеше, ваше время ожидания может сильно отличаться и быть намного дольше, чем вы, вероятно, захотите.

Это потому, что инструкции CMC друг за другом не зависят друг от друга, но инструкции CDQ имеют зависимость между ними?

Это наоборот: CMC полностью зависит от предыдущего CMC, потому что он читает и записывает флаг переноса. Точно так же, как not eax имеет истинную зависимость от предыдущего значения EAX.

CDQ этого не делает: он читает EAX и записывает EDX. Переименование регистров позволяет записывать RDX более одного раза за один такт. например Дзен может выполнять 4 cdq инструкций за такт. Ваш Coffee Lake может запускать 2 CDQ за такт (пропускная способность 0,5 с), ограничиваясь внутренними портами, на которых он может работать (p0 и p6).

Цифры Агнера Фога были основаны на тестировании огромного блока повторяющихся инструкций, что, по-видимому, ограничивало пропускную способность устаревшего декодирования, равную 1 / такт. (Снова см. Могут ли простые декодеры в новейших микроархитектурах Intel обрабатывать все 1-µOP инструкции?). Значения https://uops.info/ ближе к точным для небольшого количества повторений для Coffee Lake: оно равно 0,6. c пропускная способность. (Но если вы посмотрите на подробную разбивку с количеством разверток 500, https://www.uops.info/html-tp/CFL/CDQ-Measurements.html подтверждает, что у Coffee Lake все еще есть узкое место во внешнем интерфейсе).

Но увеличение числа повторов выше 20 (если выровнено) приведет к тому же узкому месту устаревшего декодирования, которое видел Агнер. Однако, если вы не используете lfence, декодирование может намного опережать выполнение, так что это нехорошо.

CDQ - плохой выбор из-за странных внешних эффектов и / или из-за того, что он является узким местом внутренней пропускной способности вместо задержки. Но OoO exec все еще может видеть вокруг него, как только интерфейс преодолевает повторяющиеся CDQ. 1-байтовый NOP может создать узкое место во внешнем интерфейсе, которое может быть более полезным в зависимости от того, какие две вещи вы пытаетесь разделить.


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

Если вы можете делать то, что вам нужно, просто используя зависимость данных между двумя вещами, это может уменьшить количество вещей, которые вам нужно понять, чтобы сделать что-то похожее на то, что вы описали как свою цель.

В противном случае вам, вероятно, нужно понять в основном весь этот ответ (и руководство по микроархитектуре Агнера Фога), чтобы выяснить, как ваша реальная проблема трансформируется в то, что вы действительно можете заставить процессор делать. Или поймите, что не может, и вам понадобится что-то еще. (Например, может быть, очень быстрый ЦП, работающий по порядку, возможно, ARM, где вы можете в некоторой степени контролировать время между независимыми инструкциями с помощью последовательностей / циклов задержки.)

person Peter Cordes    schedule 23.09.2020