Полное использование трубопроводов на озере Каби

(Последующий обзор кода вопрос здесь с более подробной информацией о контексте этой петли.)


Окружающая среда:

  • виндовс 7 х64
  • Сообщество VS 2017
  • Ориентация кода x64 на Intel i7700k (kaby lake)

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

Однако в моем текущем проекте оптимизатор MSVC очень плохо справляется с кодом на моем критическом пути. Так...

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

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

Что это в виду, вот ядро ​​моего самого внутреннего цикла, который (что неудивительно) находится там, где профилировщик VS говорит, что мое время тратится:

nottop:

vpminub ymm2, ymm2, ymm3 ; reset out of range values
vpsubb  ymm2, ymm2, ymm0 ; take a step

top:
vptest  ymm2, ymm1       ; check for out of range values
jnz nottop

; Outer loop that does some math, does a "vpsubb ymm2, ymm2, ymm0",
; and eventually jumps back to top

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

Вдохновленный документом Агнера об «оптимизирующем ассемблере», я придумал подход, который (надеюсь) позволяет мне выполнять 2 операции одновременно, поэтому у меня может быть один конвейер, обновляющий ymm2, а другой — обновляющий, скажем, ymm8.

Однако это нетривиальное изменение, поэтому, прежде чем я начну все разбирать, я задаюсь вопросом, может ли оно помочь. Глядя на «Таблицы инструкций» Агнера для озера Каби (моя цель), я вижу, что:

        uops
        each
        port    Latency
pminub  p01     1
psubb   p015    1
ptest   p0 p5   3

Учитывая это, похоже, что в то время как один конвейер использует p0 + p5 для выполнения vptest против ymm2, другой может использовать p1 для выполнения как vpminub, так и vpsubb на ymm8. Да, из-за vptest все еще будет складываться, но это должно помочь.

Или будет?

В настоящее время я запускаю этот код из 8 потоков (да, 8 потоков действительно дают мне лучшую общую пропускную способность, чем 4, 5, 6 или 7). Учитывая, что мой i7700k имеет 4 ядра с поддержкой Hyper-Threading, не будет ли тот факт, что на каждом ядре выполняется 2 потока, означать, что я уже максимально использую порты? Порты «на ядро», а не «на логический процессор», верно?

So.

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

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

Это (примерно) правильный взгляд на вещи? Я пропустил несколько частей? Или это в корне неправильно?


person David Wohlferd    schedule 16.07.2017    source источник
comment
Я еще не нашел хорошего инструмента, который выполняет статический анализ или анализ во время выполнения кода на ассемблере x64 с целью устранения зависаний, уменьшения задержки и т. д. Знакомьтесь: IACA. Последняя поддерживаемая им микроархитектура — Skylake, но, насколько мне известно, разница между SKL и KBL относительно невелика. (К сожалению, этот инструмент станет менее полезным в будущем, если Intel перестанет его обновлять. :-()   -  person Cody Gray    schedule 16.07.2017
comment
@CodyGray Спасибо за знакомство. Я столкнулся с iaca после того, как опубликовал этот вопрос (см. мой частичный ответ ниже). Проблема с относительно небольшой разницей заключается в том, что я понятия не имею, в чем могут заключаться эти различия. Изменения в SSE кажутся вероятной областью для улучшений при переходе от поколения 6 к поколению 7. Меня не вдохновляет идея попытаться определить, в чем могут заключаться эти различия. Помимо этого, iaca не говорит мне многого, чего я не почерпнул из работы Агнера. Это просто избавило бы меня от поиска инструкций, чтобы узнать, какие порты они используют.   -  person David Wohlferd    schedule 16.07.2017
comment
Относительно небольшая разница, возможно, является преувеличением. Насколько я знаю, у Kaby Lake та же микроархитектура, что и у Skylake, и тайминги такие же. Небольшая разница была бы чем-то вроде прыжка из Haswell в Broadwell или из Broadwell в Skylake, но Kaby Lake действительно не подходит.   -  person BeeOnRope    schedule 17.07.2017
comment
@DavidWohlferd - каковы типичные значения ymm3 и ymm0 при входе в цикл nottop? Изменяются ли они во внешнем цикле?   -  person BeeOnRope    schedule 17.07.2017
comment
@BeeOnRope - ymm0 и ymm3 являются константами, загружаемыми один раз при инициализации. Тем не менее, цель этого вопроса состояла в том, чтобы проверить мое понимание оптимизации asm. У меня есть некоторые основы, но очевидно, что мне предстоит пройти долгий путь. Я надеюсь вскоре опубликовать исполняемую версию этого кода на codereview.   -  person David Wohlferd    schedule 18.07.2017
comment
Я предполагаю, что есть действительно хороший шанс, что все это можно значительно ускорить, как только будет понято поведение внешнего цикла.   -  person BeeOnRope    schedule 18.07.2017


Ответы (2)


TL:DR: я думаю, что Hyperthreading должен держать все ваши векторные порты ALU занятыми двумя потоками на ядро.


vptest не записывает ни один векторный регистр, только флаги. Следующей итерации не нужно его ждать, поэтому его задержка в основном не имеет значения.

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


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

Давайте посмотрим на инструкции в вашем цикле. предсказание-взятие jnz всегда будет выполняться на p6, поэтому мы можем сбросить его со счетов. (Развертывание может на самом деле повредить: предсказание-не-принятие jnz также может работать на p0 или p6)

На ядре сам по себе ваш цикл должен выполняться со скоростью 2 цикла на итерацию, узким местом которого является задержка. Это 5 объединенных доменных операций, поэтому для выдачи требуется 1,25 цикла. (В отличие от test, jnz не может макросовмещаться с vptest). С гиперпоточностью внешний интерфейс уже является более серьезным узким местом, чем задержка. Каждый поток может выдавать 4 мопов в каждом втором цикле, что меньше, чем 5 мопов в каждом втором цикле узкого места цепочки зависимостей.

(Это характерно для недавнего Intel, особенно SKL/KBL: многие мопсы имеют достаточное количество портов на выбор, поэтому поддержание пропускной способности 4 мопов за такт является реалистичным, особенно с улучшенной пропускной способностью SKL для кэша мопов и декодеров, чтобы избежать проблем с пузырями из-за переднего плана. -конечные ограничения, а не внутреннее заполнение.)

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


Пропускная способность порта выполнения (незащищенный домен):

Только 1 из каждых 5 мопов работает на p6 (jnz). Это не может быть узким местом, потому что частота проблем с интерфейсом ограничивает нас менее чем одной ветвью, выдающей за такт при выполнении этого цикла.

Остальные 4 векторных ALU-оператора на итерацию должны работать на 3-х портах с векторными исполнительными устройствами. Мопы p01 и p015 обладают достаточной гибкостью планирования, так что ни один порт не будет узким местом, поэтому мы можем просто посмотреть на общую пропускную способность ALU. Это 4 мкп/итер для 3 портов для максимальной средней пропускной способности физического ядра в один итер на 1,333 цикла.

Для одного потока (без HT) это не самое серьезное узкое место. Но с двумя гиперпотоками это одна итерация на 2,6666 цикла.

Гиперпоточность должна насытить ваши исполнительные блоки, оставив часть пропускной способности внешнего интерфейса. Каждый поток должен иметь в среднем один поток на 2,666c, а внешний интерфейс может выдавать один поток на 2,5c. Поскольку задержка ограничивает вас только одним на 2c, она может наверстать упущенное после любых задержек на критическом пути из-за конфликтов ресурсов. (vptest моп крадет цикл у одного из двух других моп).

Если вы можете изменить цикл, чтобы он проверял реже или с меньшим количеством векторных операций, это может стать победой. Но все, о чем я думаю, это больше векторных операций (например, vpand вместо vptest, а затем vpor пару этих результатов вместе перед проверкой... Или vpxor для создания нулевого вектора, когда vptest будет ). Может быть, если бы был вектор XNOR или что-то в этом роде, но его нет.


Чтобы проверить, что происходит на самом деле, вы можете использовать счетчики производительности для профилирования вашего текущего кода и посмотреть, какую пропускную способность uop вы получаете для всего ядра (а не только для каждого логического потока в отдельности). Или профилируйте один логический поток и посмотрите, не заполняет ли он примерно половину p015.

person Peter Cordes    schedule 17.07.2017
comment
Чередование двух независимых цепочек отложений в одной... сложнее сделать правильно и эффективно. Я не ждал этого, но, думаю, у меня бы получилось. Гиперпоточность должна держать все ваши векторные порты ALU занятыми — ага! Поэтому, несмотря на мою неопытность, я понял это правильно. Мне нужно будет прочитать это еще раз, но сеть, похоже, такова, что, хотя цепочек зависимостей обычно следует избегать, в этом случае гиперпоточность уже максимизирует вычислительные возможности ядра (как я подозревал). Если я не могу придумать, что еще можно попробовать, я могу опубликовать большую часть этого на codereview. - person David Wohlferd; 17.07.2017
comment
@DavidWohlferd: пингуйте меня здесь, если вы опубликуете на codereview. Вопросов asm/perf так мало, что я обычно им не следую. re: обычно следует избегать цепочек зависимостей, что, очевидно, невозможно (использование выходных данных инструкций является своего рода точкой :), но идеально избегать узких мест с задержкой. Некоторые вещи по своей сути являются последовательными, и найти способы перекрытия или чередования — это круто. Особенно, когда вы также заботитесь о процессорах без HT. Я упомянул чередование в моем ответе на оптимизацию гипотезы Коллатца - person Peter Cordes; 17.07.2017
comment
Кажется, самое главное, что вы упустили, это то, что ваша цепочка отложений, переносимая петлей критического пути, составляет всего 2 цикла, а не 5, потому что vptest->jnz разветвляется на каждой итерации. Итак, у вас уже есть приличное количество ILP в одном потоке. - person Peter Cordes; 17.07.2017
comment
изменить цикл, чтобы проверять реже - не могу. или с меньшим количеством векторных мопов - Ааа... Проблема с vpand и другими (в отличие от and) в том, что они не устанавливают флаги для jnz. Мне по-прежнему нужен какой-то способ перейти от ymm к gp :(. Но сканирование инструкций SSE напомнило мне о vpmovmskb. Хотя я никогда не говорил, ymm1 — это просто старший бит каждого байта. Таким образом, vpmovmskb eax, ymm2 ; test eax, eax эффективно делает то же самое. как vptest. Предохранитель test & jnz, поэтому даже с 1 инструкцией «больше» это экономит мне ~ 5% времени выполнения. К сожалению, хотя вы дали 2 полезных ответа, я могу дать вам только 1 голос. - person David Wohlferd; 17.07.2017
comment
@DavidWohlferd: ах, да, если бы вы сказали, что я бы сказал вам сразу перейти к vpmovmskb :P Реальные варианты использования для vptest удивительно редки, так как это 2 uops и не может макросплавиться. Это может быть выигрыш, если вы используете cmov или setcc вместо ветвления по флагам, но в противном случае это только безубыточность по сравнению с vpcmpeq / vpmovmskb / test+jcc. И если вы хотите побитово сканировать или всплывать маску после выхода из цикла, у вас уже есть это в reg. (Однако без AVX ptest не является разрушительным, поэтому может спасти movdqa). - person Peter Cordes; 17.07.2017
comment
@DavidWohlferd Мне не ясно, что вы не можете сделать это быстрее, выполняя больше vpsubb инструкций подряд без vpminub или тестовых материалов, или, возможно, вы можете избежать всего внутреннего цикла путем прямого вычисления. Например, кажется, что внутренний цикл существует, когда все значения равны ‹ 127, после многократного вычитания значений в ymm0, верно? vpminub в основном позволяет избежать недополнения для тех значений, которые уже прошли эту границу? В зависимости от точного значения ymm3 кажется, что многие значения могут быть в любом случае привязаны к ymm3? - person BeeOnRope; 17.07.2017
comment
Вы можете, в принципе, просто рассчитать, сколько итераций займет цикл, напрямую: это что-то вроде max((ymm2 - 0x80) / ymm3), верно? То есть каждый элемент подразумевает определенное количество итераций, а общее количество — только самое большое из них. Теперь вы не хотите на самом деле выполнять деление, но, поскольку это байты, и особенно учитывая возможный домен значений в ymm2 и ymm3, вы можете сделать это быстро, и в этом случае вы можете просто вычислить окончательный значение напрямую (но если нет побайтового умножения SIMD, так что больше трюков). - person BeeOnRope; 18.07.2017
comment
В зависимости от соображений недополнения вы могли бы, возможно, просто вычитать N * ymm0 каждый раз, сокращая итерации на N (например, N может быть 4 или 8), а затем, когда вы обнаружите условие выхода, вы повторяете последние 4 итерации, чтобы получить правильное значение. - person BeeOnRope; 18.07.2017
comment
@BeeOnRope Моей первой реакцией было то, что это не сработает. Но это я просто защищаю свою работу. Я пытался придумать способы прямого вычисления без успеха (очевидно). Хотя это не значит, что это невозможно. И хотя количество итераций внутреннего цикла может варьироваться от 1 до ~ 103, оно имеет большой вес в сторону 1. Повторное выполнение даже при N = 2 может не быть чистой победой. Но надо попробовать, чтобы сказать наверняка. Если я смогу взломать все сторонние библиотеки и конкретное аппаратное обеспечение, которое я использую, я опубликую это на codereview. Я свяжусь с вами и Питером, когда получу его. - person David Wohlferd; 18.07.2017
comment
Если цикл обычно повторяется 1 раз, то прямое вычисление или выполнение более 1 подпрограммы за раз в большинстве случаев будет бесполезным. Странно, однако, что внутренний цикл оказался узким местом, учитывая, что внешний цикл, по-видимому, делает больше вещей, и они выполняются примерно одинаковое количество раз. Возможно, виноваты неправильные предсказания ветвления на выходе из внутреннего цикла. В Linux вы бы использовали perf или toplev.py для детализации, в Windows я не уверен, какие бесплатные инструменты лучше, но VTune — отличный инструмент, и у него есть бесплатный пробный период. - person BeeOnRope; 18.07.2017
comment
Если цикл часто запускается только один раз, много времени может быть потрачено на неверное предсказание ветвления! (Кстати, это хорошо для гиперпоточности. Не идеально, потому что некоторые ошибочно предположенные моп на самом деле запускаются и отнимают ресурсы у другого потока до того, как будет найден правильный путь, и из него должны быть выданы моп. Но ресурсы, которые в противном случае были бы бездействующими при восстановлении после ошибочного прогноза все переходят к другому потоку.) - person Peter Cordes; 18.07.2017
comment
@BeeOnRope Мне не удалось заставить работать подход N = 2. Однако теперь я опубликовал код, если вы хотите попробовать. - person David Wohlferd; 21.07.2017
comment
@PeterCordes. Вы сказали, что вам это тоже интересно (ссылка). - person David Wohlferd; 21.07.2017
comment
Спасибо @DavidWohlferd! Чтобы было ясно, при проверке кода вы открыты для более значительных алгоритмических изменений, которые по-прежнему дают тот же результат (т. е. определение вероятных простых чисел), верно? Если вам нужен точный алгоритм только с настройками внутреннего цикла, вы не станете намного лучше, чем ответ Питера здесь. Кроме того, хотя вы хорошо описываете различные низкоуровневые детали, было бы здорово иметь сводку из одного или двух предложений фактического алгоритма ProcessA, например, все вероятные простые числа в [начало, конец] идентифицируются с помощью .. . - person BeeOnRope; 21.07.2017
comment
Алгоритмические изменения @BeeOnRope. Пока я создавал этот образец для CR, я понял, что у меня есть идея, что попробовать дальше. ААР, я почти не задавал вопрос. Но я понял, что для того, чтобы решить, будет ли моя следующая идея лучше, мне нужно убедиться, что я максимально оптимизировал текущий код. Так что я думаю, что я открыт для любого. - person David Wohlferd; 21.07.2017

Частичный ответ:

Intel предоставляет инструмент под названием Intel Architecture Code Analyzer ( описан здесь), который выполняет статический анализ кода, показывая (типа), какие порты используются в разделе ассемблерного кода .

К сожалению:

  • v2.3 не включает основной (и единственный) заголовочный файл. Вы можете найти этот файл в v2.2.
  • v2.2 включает заголовок, но опускает скрипт Python (pt.py) для анализа вывода. Этот файл тоже не входит в v2.3 (еще не нашел).
  • Одним из выходных форматов iaca является файл .dot, который читается graphviz, но документы Intel не могу описать, какой из 38 исполняемых файлов в graphviz используется для отображения вывода.

Но, пожалуй, самое главное (для моих нужд):

  • v2.3 (на данный момент самая последняя версия) поддерживает Skylake, но не Kaby Lake.

Учитывая, как детали реализации меняются между процессорами, это делает весь вывод подозрительным. Даты в файле pdf предполагают, что версия 2.3 была выпущена в июле 2017 года, а это означает, что мне, вероятно, придется немного подождать следующего выпуска.

person David Wohlferd    schedule 16.07.2017
comment
Хм, странно, что они убрали заголовочный файл из версии 2.3. Как-то небрежно. Хотя приятно видеть, что в этом месяце вышла новая версия. Я где-то слышал слухи о том, что они собираются прекратить поддержку IACA, так что, надеюсь, они оказались ложными. Что касается вывода, я никогда не пробовал использовать скрипт Python. Я просто смотрю вывод текста прямо в файл (или вывожу его в командную строку). Читается, на мой взгляд, достаточно легко. Гораздо проще, чем мне сначала изучить Python или graphviz. - person Cody Gray; 16.07.2017
comment
И я бы не слишком беспокоился о Skylake и Kaby Lake. KBL — это всего лишь небольшой шаг оптимизации SKL, на самом деле просто сокращение процесса и скачок тактовой частоты. Мало что изменилось в микроархитектуре. Я думаю, что вывод будет довольно заслуживающим доверия. И, честно говоря, даже если он не идеален, он определенно поможет вам разобраться в коде так, как этого не делают руководства Агнера (если, как вы сказали, вы уже не эксперт). - person Cody Gray; 16.07.2017
comment
Хорошо, я проведу с ними еще немного времени. Там еще могут быть жемчужины. - person David Wohlferd; 16.07.2017
comment
Кстати, скрипт python предназначен для обработки вывода –trace. Но я, возможно, не готов к такому уровню детализации в любом случае. - person David Wohlferd; 16.07.2017
comment
Очень мало микроархитектуры изменилось - О, черт возьми: в озере Каби есть -плавники- (большие)! ‹включите музыку из Челюстей› - person David Wohlferd; 16.07.2017
comment
Насколько я знаю, у Kaby нет микроархитектурных улучшений по сравнению с SKL. Часы за часами идентичны SKL. Прирост производительности связан с улучшением мощности, которое позволяет ему работать в турборежиме большую часть времени. Также в iGPU есть некоторые обновления, такие как аппаратное декодирование 10-битного h.265. Вы ничего не упускаете, используя анализ SKL. Это не первый раз, когда Intel делает это: Nehalem-›Westmere был уменьшенным кристаллом с нулевыми изменениями в тактовой производительности, только в кремнии, на котором он построен. - person Peter Cordes; 17.07.2017
comment
Спасибо за публикацию об обновлении IACA! Интересно, исправили ли они какие-либо незначительные ошибки подсчета мопов для конкретных инструкций в существующих архивах... - person Peter Cordes; 17.07.2017
comment
.dot файлов можно конвертировать с помощью dot -Tpng -o output.png input.dot - person ninjalj; 17.07.2017
comment
@ninjalj - Ааа, спасибо. Раздражает, что они просто ожидают, что вы знаете, как использовать этот другой (никогда не слышал о нем) инструмент. Один пример командной строки (например, ваш) имел бы большое значение. - person David Wohlferd; 18.07.2017
comment
Я просматриваю .dot файлы в Linux с xdot как xdot FILENAME. Отлично работает с файлами IACA. - person BeeOnRope; 21.07.2017