Интерпретация абсурдно-низкой измеренной латентности в тщательном профиле (эффекты суперскалярности?)

Чтобы увеличить размер вашей фигуры в N раз, вам нужно вставить это прямо перед вашим pl.show ():

N = 2
params = pl.gcf()
plSize = params.get_size_inches()
params.set_size_inches( (plSize[0]*N, plSize[1]*N) )

Он также хорошо работает с ноутбуком ipython.

1
задан Hadi Brais 2 March 2019 в 17:27
поделиться

2 ответа

Проблема заключается в базовом подходе вычитания «задержки» 1 пустой функции, как описано:

  1. Оцените задержку функция, которая ничего не делает.
  2. Оцените задержку тестовой функции.
  3. Вычтите первое из второго, чтобы исключить затраты на выполнение служебных вызовов, тем самым приблизительно получая стоимость содержимого тестовой функции.

Встроенное допущение состоит в том, что стоимость вызова функции равна X, а если задержка работы, выполняемой в функции, равна Y, то общая стоимость будет примерно равна [110 ].

Это обычно не верно для любых двух блоков работы и особенно не верно, когда один из них «вызывает функцию». Более сложный взгляд был бы на то, что общее время будет где-то между min(X, Y) и X + Y - но даже это часто неправильно в зависимости от деталей. Тем не менее, достаточно уточнения, чтобы объяснить, что здесь происходит: стоимость функции не складывается с работой, выполняемой в функции: они происходят параллельно .

Стоимость пустого вызова функции в современном Intel составляет примерно 4–5 циклов, вероятно, в узких местах по пропускной способности внешнего интерфейса для двух взятых ветвей, и , возможно, из-за задержки предсказания ветвления и возврата.

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

Таким образом, по существу, функция будет принимать больше времени, необходимого для механики вызова функции, или фактической работы, выполняемой функцией. Это приближение не является точным, потому что некоторые типы работы могут фактически добавить к накладным расходам вызова функции (например, если есть достаточно инструкций для интерфейса перед тем, как добраться до ret, общее время может увеличиться вдобавок к 4-5 циклам времени пустой функции, даже если общая работа меньше, чем это) - но это хорошее приближение первого порядка.

Ваша первая функция занимает достаточно времени, чтобы реальная работа доминировала над временем выполнения. Вторая функция, однако, намного быстрее, что позволяет ей «прятаться» под существующим временем, затрачиваемым механиками call / ret.

Решение простое: продублируйте работу в функции N раз, чтобы работа всегда доминировала. N = 10 или N = 50 или что-то в этом роде. Вы должны решить, хотите ли вы проверить задержку, и в этом случае выходные данные одной копии работы должны поступать в следующую, или пропускную способность, в этом случае это не должно быть.

С другой стороны, если вы действительно хотите проверить стоимость вызова функции + работа, например, потому что именно так вы будете использовать ее в реальной жизни, вполне вероятно, что полученные вами результаты уже близки к корректным : вещи действительно могут быть «постепенно расширяемыми», когда они прячутся за вызовом функции.


1 Я помещаю здесь «кавычки» в кавычки, потому что не ясно, следует ли нам говорить о задержке call/ret или пропускной способности. call и ret не имеют каких-либо явных выходных данных (а ret не имеет входных данных), поэтому они не участвуют в классической цепочке зависимостей на основе регистров - но может иметь смысл подумать о задержке, если вы рассмотрим другие скрытые архитектурные компоненты, такие как указатель инструкции. В любом случае задержка пропускной способности в основном указывает на одну и ту же вещь, потому что все call и ret в потоке работают в одном и том же состоянии, поэтому не имеет смысла говорить «независимые» и «зависимые» цепочки вызовов. .

0
ответ дан BeeOnRope 2 March 2019 в 17:27
поделиться

Ваш подход к сравнительному анализу в корне неверен, а ваш «осторожный код» фальшивый.

Во-первых, очистка кэша - фальшивка. Мало того, что он будет быстро заполнен необходимыми данными, но также и у опубликованных вами примеров очень мало взаимодействия с памятью (только доступ к кешу по call/ret и нагрузка, к которой мы доберемся.

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

Теперь, когда бесполезная непредвиденная сложность исчезла, насчет фундаментального недопонимания современных процессоров:

Вы ожидаете [118 ] время, затрачиваемое на каждую итерацию цикла. Это неправильно. Современные процессоры не выполняют команды последовательно, последовательно. Современные процессоры выполняют конвейеризацию, предсказывают переходы, выполняют несколько инструкций параллельно и (достаточно быстрые процессоры) выходят из строя. .

Так что после На первой итерации цикла сравнительного анализа все ветви отлично прогнозируются на следующие почти 100000000 итераций. Это позволяет процессору спекулировать . Фактически, условная ветвь в цикле сравнительного анализа исчезает, как и большая часть стоимости косвенного вызова. Фактически, CPU может развернуть цикл:

movss  xmm0, number
movaps  xmm1, xmm0
rsqrtss xmm1, xmm0
mulss   xmm0, xmm1
movss  xmm0, number
movaps  xmm1, xmm0
rsqrtss xmm1, xmm0
mulss   xmm0, xmm1
movss  xmm0, number
movaps  xmm1, xmm0
rsqrtss xmm1, xmm0
mulss   xmm0, xmm1
...

или, для другого цикла

movss  xmm0, number
sqrtss  xmm0, xmm0
movss  xmm0, number
sqrtss  xmm0, xmm0
movss  xmm0, number
sqrtss  xmm0, xmm0
...

Заметим, что загрузка number всегда одинакова (таким образом, быстро кэшируется) , и перезаписывает только что вычисленное значение, разрывая цепочку зависимостей .

Чтобы быть справедливым,

call   rbp
sub    rbx, 0x1
jne    15b0 <double profile(float)+0x20>

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

Если вы ожидаете, что ЦП будет выполнять инструкции последовательно, ЦП может вместо этого выполнять их параллельно.

Небольшой взгляд на https://agner.org/optimize/instruction_tables.pdf показывает, почему это параллельное выполнение не работает для sqrtss на Nehalem:

instruction: SQRTSS/PS
latency: 7-18
reciprocal throughput: 7-18
[1139 ] т.е. инструкция не может быть передана по конвейеру и выполняется только на одном порту выполнения. Напротив, для movaps, rsqrtss, mulss:

instruction: MOVAPS/D
latency: 1
reciprocal throughput: 1

instruction: RSQRTSS
latency: 3
reciprocal throughput: 2

instruction: MULSS
latency: 4
reciprocal throughput: 1

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

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

float x = INITIAL_VALUE;
for (i = 0; i < 100000000; i++)
    x = benchmarked_function(x);

Очевидно, что вы не станете сравнивать один и тот же ] введите таким образом, если только INITIAL_VALUE не является фиксированной точкой benchmarked_function(). Тем не менее, вы можете организовать , чтобы она была фиксированной точкой расширенной функции, вычислив float diff = INITIAL_VALUE - benchmarked_function(INITIAL_VALUE); и затем сделав цикл

float x = INITIAL_VALUE;
for (i = 0; i < 100000000; i++)
    x = diff + benchmarked_function(x);

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

0
ответ дан EOF 2 March 2019 в 17:27
поделиться
Другие вопросы по тегам:

Похожие вопросы: