(Последующий обзор кода вопрос здесь с более подробной информацией о контексте этой петли.)
Окружающая среда:
- виндовс 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, кто-нибудь мог бы предложить альтернативный подход. Но цель этого Вопроса не в том, чтобы кто-то написал мой код за меня. Я пытаюсь понять, начинаю ли я понимать, как думать об оптимизации ассемблерного кода.
Это (примерно) правильный взгляд на вещи? Я пропустил несколько частей? Или это в корне неправильно?
ymm3
иymm0
при входе в циклnottop
? Изменяются ли они во внешнем цикле? - person BeeOnRope   schedule 17.07.2017