Ещё полезная опция (под Windows по крайней мере) /XJ — Exclude Junction Points. С этой опцией robocopy будет пропускать символические ссылки и точки соединения каталогов (типа Application Data) , иначе оно может зациклиться, если весь диск C:\ начать копировать.
если запустить одиночное умножение на двух логических ядрах одного физического, то они сразу тормозят друг друга,
Всё чудесатее и чудесатее!
Всё, я понял, куда упёрлись.
Это требует один такт:
.loop:
vmulps ymm2, ymm1, ymm0
dec r8
jnz .loop
Два таких цикла в параллель на гипертредированных ядрах никогда не отработают за один такт на Haswell, потому что он не умеет делать два джампа за один такт. Дело в JNZ, а не VMULPS. Если положить два VMULPS - ровно тоже, у JNZ нет свободного такта
А вот в этом случае есть - такой цикл отрабатывается за два такта:
И если запустить их два в параллель, то будет не по четыре, а только по три, потому что JNZ будет отрабатывать в пустом, свободном от JNZ слоте другого потока. JNZ, кстати, фьюзится вместе с декрементом и они вместе отрабатывают на шестом порту, uiCA рулит.
Бонус для тех, кто промотал комментарии до этого места - в том, месте, где умножение было заменено на сложение цикл замедлялся, но истинная причина была совершенно не в сложении (впрочем я этого и не писал). Умные люди мне подсказали, что просто эта операция выполняется за один такт, но там дальше идёт переход jnz (который параллелится с add). Так вот, когда этот цикл крутится на одном ядре, то всё так и есть - один такт на цикл проца, но когда ему в нагрузку мы придаём второй цикл с imul, то там тоже jnz, и вместе они отрабатывать не могут, Haswell не умеет сделать два условных перехода в пределах одного своего цикла, поэтому ему приходится притормаживать цикл с add, чтобы выполнить переход в тот момент, когда цикл свободен от jnz (а таких два свободных, потому что imul требует три такта, один из которых отрабатывает в параллель с jnz, а два других "свободны"). Вот так-то. А когда там два умножения были, то каждый имел запас и в установившемся режиме они исполнялись со сдвигом на такт. Процессор - офигенно интересная штука, на самом деле. Ну и мораль - чем меньше в коде условных переходов, тем лучше (но это было и так известно), и дело не только в предсказателе переходов, но и в гипертрединге.
Да, конечно, есть разные уровни оптимизации от "нано" до "макро", когда либо используется параллелизация на уровне команд (что сложно и зависит от архитектуры), затем идёт SIMD, затем можно одно изображение разбивать на части для потоков и самое простое — один поток-одна картинка, если их много надо обрабатывать; разумно комбинировать эти методы для достижения максимальной производительности в общем не возбраняется.
О, здесь как раз всё очень просто. Смотрите, прямо сейчас мне нужен быстрый алгоритм свёртки (convolution) - 5х5 да 7х7. Картинка 4096х4096 бьётся на полоски 4096х1024 (или 4096х512 если на восемь потоков), дальше алгоритм бежит этими потоками по исходной картинке и пишет результат в неперекрывающиеся области памяти целевой картинки, так что после завершения всех потоков у меня есть отфильтрованное изображение, мне специально собирать ничего не надо, а момент окончания работы всех потоков - это банальный WaitForMultipleObjects, который я в спинлоке использовал в комментах ниже , вот и всё. У меня на данный момент в руках есть три библиотечных реализации свёртки - OpenCV, Intel IPP и NI VDM (и, кстати, NI самый быстрый), но у меня есть стойкое ощущение, что я могу ещё быстрее, применяя знания из статьи (и нет, гипертрединг мне тут не нужен). Короче, если я смогу написать реализацию быстрее любой известной мне, то, безусловно, напишу статью, ну а нет — то нет.
На самом деле не совсем теория, у меня задачи связаны в основном с промышленным машинным зрением (причём рентгеновским), это значит в памяти картинки 4096х4096 либо 16 бит либо float, и их надо всякими разными способами обрабатывать, причём в почти реальном времени ("почти" означает промышленный конвейер, и я должен должен успевать, иначе начальник смены будет сильно недоволен). Картинки бьются на части, которые обрабатываются несколькими потоками и это практически то, о чём я выше написал (только у меня не sha256 а свёртка или там медианная фильтрация и т.п), ну и ядер не 20 а 64 или больше.
В общем да. Чтобы добавить, я могу показать пример - вот SHA256 из статьи, но на 10-ти ядернике (у него 20 логических).
Я запущу тест три раза и каждый раз буду запускать всё больше потоков - от одного до двадцати. Один раз я дам ОС выбрать самой где запускать, второй раз вначале я буду задействовать только физические ядра, и лишь потом - логические, а третий - строго последовательно, то есть одно физическое, затем логическое и т.д. и каждый раз буду смотреть суммарную производительность. Вот что получится:
Одиночный поток выдаёт на этом процессоре где-то 160-170 МБ/с, и если задействовать только физические, то производительность растёт более-менее линейно до 1700 на десяти ядрах, дальше насыщение. Когда ОС выбирает сама, то до шести потоков у неё всё более-менее, но затем её "трясёт" и на 1700 она выходит лишь на 17 потоках, ну а при последовательном занятии ядер видны чёткие чуть растущие ступеньки - это как раз каждое второе гипертредированное ядро задействуется. А смещение вверх от средней линии (зелёная "сидит" практически на ней) — это как раз насколько система "врёт" когда рапортует о процентах. Как-то так.
Если я ошибся, то это, бомба, мне придётся подводить под это совершенно другую теорию, но не думаю, я слишком много тестов сделал, осбенно проседающий на соседних ядрах SHA256 об этом говорит. Можно, конечно упороться, найти и развернуть системы от W2K до последней и отследить на какой из них точно произошло изменение, но ИИ мне пишет вот что: "Изменение в поведении масок привязки процессора и нумерации логических процессоров — особенно в контексте Hyper-Threading и групп процессоров — произошло с введением групп процессоров в Windows Server 2008 R2 и Windows 7. " поверим ему на слово.
ThreadProc PROC
WinABI GetCurrentThread ; This one will be in RAX
WinABI SetThreadAffinityMask, RAX, RCX ; RCX is thread param (core#)
mov r8, [Increments]
До сих пор я был уверен, что первые два бита - это два логические ядра одного физического, следующие два - это следующий проц и так далее. Это не номера, это битовая маска, поэтому валидные значения для выполенния на одном ядре 1, 2, 4, 8 и так далее. Если вызвать spin_lock.exe 255 255 то два потока будут на восьми ядрах сидеть (в смысле система их сама будет перебрасывать междя ядрами своим планировщиком). Это маски, не номера. А на Линуксе они по-другому нумеруются, это да, комментом ниже отметили.
Спасибо! И, кстати, неплохо, тут "пенальти" где-то в 1,6х раза, а у меня больше чем четырёхкратные, впрочем на современных процессорах я не тестил, на i7-10850H @ 2,7 GHz вот так:
Способ тюнинга планировщика под Windows мне неизвестен, но в принципе мы можем оценить примрно как часто происходит переключение между потоками кодом типа этого:
#include <windows.h>
#include <stdio.h>
#include <math.h>
#include <timeapi.h>
int main()
{
timeBeginPeriod(1); // чтобы была миллисекунда в Sleep, а не 15,6
double time_min = INFINITY, time_max = 0;
unsigned int last_core = 0xFF;
unsigned __int64 last_tsc = 0, current_tsc, start_tsc;
unsigned int current_core;
bool first_run = true;
start_tsc = last_tsc = current_tsc = __rdtscp(¤t_core);
while((current_tsc - start_tsc) < 3600000000) {
current_tsc = __rdtscp(¤t_core);
Sleep(1); // не грузим проц понапрасну
if (last_core != 0xFFFFFFFF && current_core != last_core && !first_run) {
__int64 diff = current_tsc - last_tsc;
double time_ms = (double)diff / 3600000.0; // Convert ticks to ms
printf("core switch %d > %d, time: %.3f ms\n", last_core, current_core, time_ms);
if (time_ms > time_max) time_max = time_ms;
if (time_ms < time_min) time_min = time_ms;
last_tsc = current_tsc;
}
last_core = current_core;
first_run = false;
}
printf("Min time = %.3f ms; Max Time = %.3f ms;\n", time_min, time_max);
return 0;
}
И вот я вижу, что переключение происходит примрно так (time это сколько мы сидим на одном ядре):
core switch 4 > 1, time: 1.433 ms
core switch 1 > 2, time: 13.927 ms
core switch 2 > 0, time: 1.448 ms
core switch 0 > 2, time: 1.757 ms
core switch 2 > 4, time: 4.021 ms
core switch 4 > 2, time: 1.342 ms
core switch 2 > 4, time: 3.891 ms
core switch 4 > 2, time: 1.270 ms
core switch 2 > 4, time: 6.976 ms
core switch 4 > 2, time: 1.232 ms
core switch 2 > 4, time: 5.234 ms
core switch 4 > 2, time: 2.310 ms
Min time = 1.106 ms; Max Time = 13.927 ms;
Времена меньше одной миллисекундя я там не ловлю, потому что Sleep(1). Ну то есть при десяток миллисекунд я в общем не соврал, больше 14 мс мы на одном ядре не сидели
Если же убрать Sleep(1) из кода и тем самым загнать поток в 100% использование процессора, то будет так:
core switch 2 > 6, time: 0.186 ms
core switch 6 > 7, time: 29.554 ms
core switch 7 > 4, time: 0.224 ms
core switch 4 > 6, time: 120.686 ms
core switch 6 > 7, time: 537.447 ms
core switch 7 > 6, time: 0.214 ms
core switch 6 > 4, time: 0.143 ms
core switch 4 > 2, time: 0.116 ms
Min time = 0.116 ms; Max Time = 537.447 ms;
То есть мы можем и полсекунды провести на одном ядре, а можем и десять раз в миллисекунду переброситься. В общем не особо беспокоит, так как время переброса относительно невелико (где-то я читал про несколько микросекунд, в общем тоже можно попробовать оценить, но лень), Это детально вроде нигде не документированно, мы можем лишь подвергать систему внешнему воздействию нашим кодом и смотреть её реакцию. Я, кстати, не поленился проверить на Windows Server 2019, и там всё ровно также работает, хотя вроде везде пишут, что там планировщик чуть по-другому тюнингован.
Справедливости ради, даже эзотерическая LabVIEW бодро рапортует, сколько у неё физических и логических ядер доступно, даже про кеш отчитывается, так что если горе-программист зашёл не в те ядра, то сам себе злобный Буратино
Чуть сложнее с P и Е ядрами, но такого компа я пока в руках не держал.
у вас есть возможность там позапускать vtune amplifier?
VTune есть, но он слегка бастует и говорит, что камушек не поддерживается. Ну то есть хотспот он показывает, но это мало о чём говорит, мы это и без него знаем:
В соседней ветке комментов мы более-менее с кешем разобрались, я мог бы и тут через Intel PCM глянуть, но я совершенно ХЗ в какой из ивентов смотреть. Загрузку по портам я глянул, ну да, они заняты, но там возможно что-то ещё.
Круто, заодно и с флагами оптимизации слегка разобрались, а то я всегда сомневался - чем же О1 от О2 отличается, да как-то руки не доходили. Профит от таких упражнений ещё и в том, что немножко прокачиваются скиллы в смежных областях.
До кучи "LabView" написано неправильно, должно быть "LabVIEW".
Ещё полезная опция (под Windows по крайней мере)
/XJ
— Exclude Junction Points. С этой опцией robocopy будет пропускать символические ссылки и точки соединения каталогов (типа Application Data) , иначе оно может зациклиться, если весь диск C:\ начать копировать.Всё, я понял, куда упёрлись.
Это требует один такт:
Два таких цикла в параллель на гипертредированных ядрах никогда не отработают за один такт на Haswell, потому что он не умеет делать два джампа за один такт. Дело в JNZ, а не VMULPS. Если положить два VMULPS - ровно тоже, у JNZ нет свободного такта
А вот в этом случае есть - такой цикл отрабатывается за два такта:
И если запустить их два в параллель, то будет не по четыре, а только по три, потому что JNZ будет отрабатывать в пустом, свободном от JNZ слоте другого потока. JNZ, кстати, фьюзится вместе с декрементом и они вместе отрабатывают на шестом порту, uiCA рулит.
Бонус для тех, кто промотал комментарии до этого места - в том, месте, где умножение было заменено на сложение цикл замедлялся, но истинная причина была совершенно не в сложении (впрочем я этого и не писал). Умные люди мне подсказали, что просто эта операция выполняется за один такт, но там дальше идёт переход jnz (который параллелится с add). Так вот, когда этот цикл крутится на одном ядре, то всё так и есть - один такт на цикл проца, но когда ему в нагрузку мы придаём второй цикл с imul, то там тоже jnz, и вместе они отрабатывать не могут, Haswell не умеет сделать два условных перехода в пределах одного своего цикла, поэтому ему приходится притормаживать цикл с add, чтобы выполнить переход в тот момент, когда цикл свободен от jnz (а таких два свободных, потому что imul требует три такта, один из которых отрабатывает в параллель с jnz, а два других "свободны"). Вот так-то. А когда там два умножения были, то каждый имел запас и в установившемся режиме они исполнялись со сдвигом на такт. Процессор - офигенно интересная штука, на самом деле. Ну и мораль - чем меньше в коде условных переходов, тем лучше (но это было и так известно), и дело не только в предсказателе переходов, но и в гипертрединге.
Да, конечно, есть разные уровни оптимизации от "нано" до "макро", когда либо используется параллелизация на уровне команд (что сложно и зависит от архитектуры), затем идёт SIMD, затем можно одно изображение разбивать на части для потоков и самое простое — один поток-одна картинка, если их много надо обрабатывать; разумно комбинировать эти методы для достижения максимальной производительности в общем не возбраняется.
О, здесь как раз всё очень просто. Смотрите, прямо сейчас мне нужен быстрый алгоритм свёртки (convolution) - 5х5 да 7х7. Картинка 4096х4096 бьётся на полоски 4096х1024 (или 4096х512 если на восемь потоков), дальше алгоритм бежит этими потоками по исходной картинке и пишет результат в неперекрывающиеся области памяти целевой картинки, так что после завершения всех потоков у меня есть отфильтрованное изображение, мне специально собирать ничего не надо, а момент окончания работы всех потоков - это банальный WaitForMultipleObjects, который я в спинлоке использовал в комментах ниже , вот и всё. У меня на данный момент в руках есть три библиотечных реализации свёртки - OpenCV, Intel IPP и NI VDM (и, кстати, NI самый быстрый), но у меня есть стойкое ощущение, что я могу ещё быстрее, применяя знания из статьи (и нет, гипертрединг мне тут не нужен). Короче, если я смогу написать реализацию быстрее любой известной мне, то, безусловно, напишу статью, ну а нет — то нет.
На самом деле не совсем теория, у меня задачи связаны в основном с промышленным машинным зрением (причём рентгеновским), это значит в памяти картинки 4096х4096 либо 16 бит либо float, и их надо всякими разными способами обрабатывать, причём в почти реальном времени ("почти" означает промышленный конвейер, и я должен должен успевать, иначе начальник смены будет сильно недоволен). Картинки бьются на части, которые обрабатываются несколькими потоками и это практически то, о чём я выше написал (только у меня не sha256 а свёртка или там медианная фильтрация и т.п), ну и ядер не 20 а 64 или больше.
В общем да. Чтобы добавить, я могу показать пример - вот SHA256 из статьи, но на 10-ти ядернике (у него 20 логических).
Я запущу тест три раза и каждый раз буду запускать всё больше потоков - от одного до двадцати. Один раз я дам ОС выбрать самой где запускать, второй раз вначале я буду задействовать только физические ядра, и лишь потом - логические, а третий - строго последовательно, то есть одно физическое, затем логическое и т.д. и каждый раз буду смотреть суммарную производительность. Вот что получится:
Одиночный поток выдаёт на этом процессоре где-то 160-170 МБ/с, и если задействовать только физические, то производительность растёт более-менее линейно до 1700 на десяти ядрах, дальше насыщение. Когда ОС выбирает сама, то до шести потоков у неё всё более-менее, но затем её "трясёт" и на 1700 она выходит лишь на 17 потоках, ну а при последовательном занятии ядер видны чёткие чуть растущие ступеньки - это как раз каждое второе гипертредированное ядро задействуется. А смещение вверх от средней линии (зелёная "сидит" практически на ней) — это как раз насколько система "врёт" когда рапортует о процентах. Как-то так.
С этим я совершенно согласен, более того, есть ещё NUMA, а там можно огрести реальные пенальти, просто обратившись к банку памяти "чужого" сокета.
Если я ошибся, то это, бомба, мне придётся подводить под это совершенно другую теорию, но не думаю, я слишком много тестов сделал, осбенно проседающий на соседних ядрах SHA256 об этом говорит. Можно, конечно упороться, найти и развернуть системы от W2K до последней и отследить на какой из них точно произошло изменение, но ИИ мне пишет вот что: "Изменение в поведении масок привязки процессора и нумерации логических процессоров — особенно в контексте Hyper-Threading и групп процессоров — произошло с введением групп процессоров в Windows Server 2008 R2 и Windows 7. " поверим ему на слово.
Нет, бенчмарк тупо передаёт два числа, что ему передали в SetThreadAffinityMask() через параметр dwThreadAffinityMask, а дальше WinAPI отрабатывает, это вот здесь происходит:
До сих пор я был уверен, что первые два бита - это два логические ядра одного физического, следующие два - это следующий проц и так далее. Это не номера, это битовая маска, поэтому валидные значения для выполенния на одном ядре 1, 2, 4, 8 и так далее. Если вызвать
spin_lock.exe 255 255
то два потока будут на восьми ядрах сидеть (в смысле система их сама будет перебрасывать междя ядрами своим планировщиком). Это маски, не номера. А на Линуксе они по-другому нумеруются, это да, комментом ниже отметили.Спасибо! И, кстати, неплохо, тут "пенальти" где-то в 1,6х раза, а у меня больше чем четырёхкратные, впрочем на современных процессорах я не тестил, на i7-10850H @ 2,7 GHz вот так:
Способ тюнинга планировщика под Windows мне неизвестен, но в принципе мы можем оценить примрно как часто происходит переключение между потоками кодом типа этого:
И вот я вижу, что переключение происходит примрно так (time это сколько мы сидим на одном ядре):
Времена меньше одной миллисекундя я там не ловлю, потому что Sleep(1). Ну то есть при десяток миллисекунд я в общем не соврал, больше 14 мс мы на одном ядре не сидели
Если же убрать Sleep(1) из кода и тем самым загнать поток в 100% использование процессора, то будет так:
То есть мы можем и полсекунды провести на одном ядре, а можем и десять раз в миллисекунду переброситься. В общем не особо беспокоит, так как время переброса относительно невелико (где-то я читал про несколько микросекунд, в общем тоже можно попробовать оценить, но лень), Это детально вроде нигде не документированно, мы можем лишь подвергать систему внешнему воздействию нашим кодом и смотреть её реакцию. Я, кстати, не поленился проверить на Windows Server 2019, и там всё ровно также работает, хотя вроде везде пишут, что там планировщик чуть по-другому тюнингован.
Нет проблем, вот исходник., попробуйте
Я там добавил аргументов командной строки по-быстрому, чтоб не плодить экзешники и было удобнее пользоваться. Собранный бинарник в пре-релизе.
Использовать так:
spin_lock.exe Affinity1 Affinity2 Increments
Если на HT ядрах, то spin_lock.exe 1 2 1000000
Если на физических то spin_lock.exe 1 4 1000000
Вирусов там нет, но некоторые эвристики могут нервно реагировать, тут уж извините.
А, блин, слона-то я и не заметил, всё понятно.
Справедливости ради, даже эзотерическая LabVIEW бодро рапортует, сколько у неё физических и логических ядер доступно, даже про кеш отчитывается, так что если горе-программист зашёл не в те ядра, то сам себе злобный Буратино
Чуть сложнее с P и Е ядрами, но такого компа я пока в руках не держал.
VTune есть, но он слегка бастует и говорит, что камушек не поддерживается. Ну то есть хотспот он показывает, но это мало о чём говорит, мы это и без него знаем:
В соседней ветке комментов мы более-менее с кешем разобрались, я мог бы и тут через Intel PCM глянуть, но я совершенно ХЗ в какой из ивентов смотреть. Загрузку по портам я глянул, ну да, они заняты, но там возможно что-то ещё.
похоже это оно и есть.
У меня VTune 2025.5 чё-то чудит на этой тачке и говорит, что проц не поддерживается (хотя по спекам должен бы)
Но есть ведь ещё Intel PCM, я его так запускаю, данные взял отсюда:
И вот, при работе по гипертредированным ядрам (первые два), когда всё быстро:
Ну то есть там их кот наплакал, а вот при работе по физическим, то есть через ядро, когда всё тормозит:
Да, там овердофига ивентов.
Откуда вы всё это знаете?!
Имеено так, сам в шоке. И на i7-10850H ровно тоже самое. Проверьте сами — код выше.
Медитативная гифка-пруф на полторы минуты, сначала HT, секунд этак 25, а потом два физических — больше минуты:
Код строго одинаковый, из коммента выше, разница только в аффинити.
Круто, заодно и с флагами оптимизации слегка разобрались, а то я всегда сомневался - чем же О1 от О2 отличается, да как-то руки не доходили. Профит от таких упражнений ещё и в том, что немножко прокачиваются скиллы в смежных областях.