All streams
Search
Write a publication
Pull to refresh
230
204.5
Андрей Дмитриев @AndreyDmitriev

Пользователь

Send message

До кучи "LabView" написано неправильно, должно быть "LabVIEW".

Ещё полезная опция (под Windows по крайней мере) /XJ — Exclude Junction Points. С этой опцией robocopy будет пропускать символические ссылки и точки соединения каталогов (типа Application Data) , иначе оно может зациклиться, если весь диск C:\ начать копировать.

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

Всё чудесатее и чудесатее!

Всё, я понял, куда упёрлись.

Это требует один такт:

.loop:
	vmulps ymm2, ymm1, ymm0 
	dec r8
	jnz .loop

Два таких цикла в параллель на гипертредированных ядрах никогда не отработают за один такт на Haswell, потому что он не умеет делать два джампа за один такт. Дело в JNZ, а не VMULPS. Если положить два VMULPS - ровно тоже, у JNZ нет свободного такта

А вот в этом случае есть - такой цикл отрабатывается за два такта:

.loop:
	vmulps ymm2, ymm1, ymm0 
	vmulps ymm5, ymm4, ymm3 
	vmulps ymm8, ymm7, ymm6 
	dec r8
	jnz .loop

И если запустить их два в параллель, то будет не по четыре, а только по три, потому что 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 отрабатывает, это вот здесь происходит:

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 вот так:

C:\Users\Andrey\Desktop>spin_lock.exe 1 2 1000000
SpinLockTest for increments: 1000000
49251103 Ticks; Check = 2000000

C:\Users\Andrey\Desktop>spin_lock.exe 1 4 1000000
SpinLockTest for increments: 1000000
165250294 Ticks; Check = 2000000

C:\Users\Andrey\Desktop>spin_lock.exe 1 2
SpinLockTest for increments: 50000000
2243564608 Ticks; Check = 100000000

C:\Users\Andrey\Desktop>spin_lock.exe 1 4
SpinLockTest for increments: 50000000
9282440451 Ticks; Check = 100000000

Способ тюнинга планировщика под 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(&current_core);
	while((current_tsc - start_tsc) < 3600000000) {
    	current_tsc = __rdtscp(&current_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, и там всё ровно также работает, хотя вроде везде пишут, что там планировщик чуть по-другому тюнингован.

Нет проблем, вот исходник., попробуйте

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

Использовать так:

spin_lock.exe Affinity1 Affinity2 Increments

Если на HT ядрах, то spin_lock.exe 1 2 1000000

Если на физических то spin_lock.exe 1 4 1000000

Вирусов там нет, но некоторые эвристики могут нервно реагировать, тут уж извините.

А, блин, слона-то я и не заметил, всё понятно.

Справедливости ради, даже эзотерическая LabVIEW бодро рапортует, сколько у неё физических и логических ядер доступно, даже про кеш отчитывается, так что если горе-программист зашёл не в те ядра, то сам себе злобный Буратино

Чуть сложнее с P и Е ядрами, но такого компа я пока в руках не держал.

у вас есть возможность там позапускать vtune amplifier?

VTune есть, но он слегка бастует и говорит, что камушек не поддерживается. Ну то есть хотспот он показывает, но это мало о чём говорит, мы это и без него знаем:

В соседней ветке комментов мы более-менее с кешем разобрались, я мог бы и тут через Intel PCM глянуть, но я совершенно ХЗ в какой из ивентов смотреть. Загрузку по портам я глянул, ну да, они заняты, но там возможно что-то ещё.

каунтеры по зачитыванию модифицированных данных из L3 (скажем, MEM_LOAD_UOPS_L3_HIT_RETIRED.XSNP_HITM

похоже это оно и есть.

У меня VTune 2025.5 чё-то чудит на этой тачке и говорит, что проц не поддерживается (хотя по спекам должен бы)

Но есть ведь ещё Intel PCM, я его так запускаю, данные взял отсюда:

pcm-core.exe -e cpu/umask=0x04,event=0xD2,name=MEM_LOAD_UOPS_L3_HIT_RETIRED.XSNP_HITM/

И вот, при работе по гипертредированным ядрам (первые два), когда всё быстро:

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

Да, там овердофига ивентов.

Откуда вы всё это знаете?!

Имеено так, сам в шоке. И на i7-10850H ровно тоже самое. Проверьте сами — код выше.

Медитативная гифка-пруф на полторы минуты, сначала HT, секунд этак 25, а потом два физических — больше минуты:

Код строго одинаковый, из коммента выше, разница только в аффинити.

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

1
23 ...

Information

Rating
28-th
Location
Ahrensburg, Schleswig-Holstein, Германия
Date of birth
Registered
Activity