2014 dxdy logo

Научный форум dxdy

Математика, Физика, Computer Science, Machine Learning, LaTeX, Механика и Техника, Химия,
Биология и Медицина, Экономика и Финансовая Математика, Гуманитарные науки




Начать новую тему Ответить на тему На страницу Пред.  1 ... 5, 6, 7, 8, 9

Нужна ли такая тема про ассемблер?
Опрос закончился 24.01.2024, 03:22
Да, почитаю. 50%  50%  [ 12 ]
Да, поспрашиваю. 25%  25%  [ 6 ]
Да, поотвечаю. 4%  4%  [ 1 ]
Мне всё равно, но не против, дерзайте. 17%  17%  [ 4 ]
Нет, не интересно, полно готовой литературы. 0%  0%  [ 0 ]
Нет, ничего в этом не понимаю и не собираюсь. 0%  0%  [ 0 ]
Нет, форум не про это, есть другие более подходящие. 4%  4%  [ 1 ]
Другое ... 0%  0%  [ 0 ]
Всего голосов : 24
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение23.01.2024, 19:06 
Аватара пользователя


29/04/13
7227
Богородский
Dmitriy40 в сообщении #1626872 писал(а):
У меня проблема с КТО в другом: мне надо очень быстро вычислять не просто вектор z[], а матрицу z[]%p[] для хотя бы пары-тройки десятков простых p[] для каждого элемента z[]. И как вот это делать без делений (а лучше и без умножений) и имея лишь нечто типа матриц x[]%p[] и y[]%p[] - совсем непонятно.

Вот я даже не знаю, где лучше разговаривать, здесь или здесь, где я как раз начал более тщательно разбираться с остатками.

vpb, спасибо. См. ту же тему.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение24.01.2024, 01:40 
Заслуженный участник


20/08/14
11177
Россия, Москва
Начну пожалуй про модулярную арифметику в приложении к поиску последовательностей простых по заданному паттерну (расстоянию между соседними простыми). И для примера возьму цепочку из 4-х простых с интервалами 2,4,2, или в другой записи 0,2,6,8 - две максимально близкие пары простых близнецов. Как Yadryara объяснил в другой теме, из каждых 30 последовательных чисел подходит лишь одно, с остатком 11 от деления на 30 (только с такого числа может начинаться искомая цепочка). Руками не считал, нагрузил расчётом PARI:
? chinese([Mod(1,2),Mod(2,3),Mod(1,5)])
%1 = Mod(11, 30)

Первые решения:
Используется синтаксис Text
C:\>primesieve.x64con.exe 0 -d1000 -p4
(5, 7, 11, 13)
(11, 13, 17, 19)
(101, 103, 107, 109)
(191, 193, 197, 199)
(821, 823, 827, 829)
Первая строка отпадает - это исключение, остаток по модулю 30 другой, но такие исключения могут быть только до числа 30, не дальше, на это плюнем.

Так как между искомыми простыми никаких других простых быть не может в принципе, то можно просто идти по всем натуральным числам с 11 с шагом 30 и проверять их на простоту. Если простое - проверять остальные три числа на простоту. Для чисел около 4млрд (доступных нам пока что для проверок на простоту) простым является примерно каждое 11 нечётное число ($\ln(2^{32})/2$), а мы будем проверять лишь каждое 15-е из них, то в среднем первое число цепочки окажется простым один раз из 165. Разумеется это намного медленнее решета Эратосфена, но это же чисто модельный пример.
Все пояснения про цепочки, паттерны, допустимые остатки, модули - всё в другой теме (например указанной Yadryara выше), здесь займёмся приложением математики к ассемблеру.

Вспомним что значит простота числа: это значит что число не делится на простые меньшие него (корня, не корня - не суть). Попробуем отказаться от проверки делимости первого числа цепочки на 7 (на меньшие простые не делится гарантированно и их проверять не надо). Число делится на простое если остаток от деления равен 0. Но остаток 11%7=4 мы знаем (или можем легко посчитать один раз). Раз он не 0, то и 11 на 7 не делится (кто бы сомневался).
А теперь фокус: а будет ли делиться на 7 следующее проверяемое число? А какое оно? Ну так 11+30=41. Конечно всем с 3-го класса очевидно что не делится. А компьютер в школе не учили и ему не очевидно! И делить мы не хотим. Сделаем так: 41%7=(11+30)%7=(11%7+30%7)%7. Казалось бы что в этом хорошего, одно деление превратили в три? Но ведь не в три, 11%7 мы и так уже знали на предыдущей итерации перебора, это 4. 30%7=2 мы тоже знаем (или можем легко посчитать), и оно не будет меняться ни для каких проверяемых нами чисел, значит его можно вычислить один раз в начале. Остаётся последнее взятие остатка. Но смотрим внимательно, в скобках оба слагаемых меньше модуля, их сумма уж всяко меньше удвоенного модуля, значит делить не обязательно, достаточно сравнить с 7 и если превышает, то вычесть 7 чтобы результат стал всегда меньше 7 (модуля), и это гораздо проще чем делить.
При повторении прибавления шага 30 снова сработает ровно тот же фокус.
Приходим к следующей проверке делимости первого числа цепочки на 7:
Используется синтаксис C
x=11; m=30;
x7=x%7; m7=m%7;
for(i=1; i<33; ++i) {//Все числа <30 пропустим и начнём сразу дальше
  x7=x7+m7;
  if(x7>=7) x7=x7-7;
  if(x7!=0) printf("First number is not div by 7 at i= %u",i);
}
Ну, начальная инициализация и цикл нам не интересны, как и printf, речь про две строки вычисления x7 без делений.
Вариант на асме вычисления x7 (в ESI), с условным переходом должен быть понятен всем:
Используется синтаксис ASM
;Вариант с условным переходом 
        add     ESI,m7          ;x7=x7+m7
        cmp     ESI,7
        jc      .nsub
        sub     ESI,7
.nsub:
;Вариант без перехода
        add     ESI,m7          ;x7=x7+m7
        mov     EAX,ESI
        sub     EAX,7
        cmovnc  ESI,EAX
А вот второй вариант интереснее, есть лишняя mov (в таком коде по идее совершенно бесплатная) и непонятная cmovnc - а она скопирует EAX в ESI только если перенос будет =0 (после cmp, а значит ESI>=7), иначе не сделает ничего. Тут не только не нужна метка (программисты все лентяи), но и работать должно быстрее - любой условный переход заметно тормозит программу если его нельзя предсказать более чем в 90% случаев (будет он выполнен или нет), а тут его нет. В данном конкретном случае у нас m7=2 и переход будет выполняться с вероятностью 5/7 (лишь два остатка из 7 возможных после прибавления двойки дадут число больше или равное 7 и переход выполнен не будет). С такой вероятностью предсказание перехода будет ошибаться раза 4 из 7, т.е. даже больше половины, и каждый раз будет тормозить программу. А команда cmov программу не тормозит. На моём процессоре (от 2013г) этот код должен выполняться за 1 такт, все 4 команды. Вместо тактов 10 для команды div.
Фактически связка команд mov (бесплатная) + cmp + cmov превращает предыдущее сложение в сложение по модулю.
Ещё причина избавиться от условного перехода: его нет в MMX/SSE/AVX, а мы целимся их применить для ускорения, ну если получится.

Итак, при последовательном переборе с неким постоянным шагом мы можем вычислять остатки по модулю простых достаточно быстро (и без делений).
Дальше мы можем с остатком делать что угодно: хоть проверить на 0 и если да, то остальные числа в цепочке не проверять (первое же кратно 7 и следовательно не простое), хоть командами bt + jnc взять признак разрешённого конкретно такого остатка по модулю простого для всей цепочки (их 3шт: 2,3,4) и остальные числа на делимость на 7 вообще не проверять, хоть ещё что-нибудь забавное.

А в SSE4.1 и AVX есть команда pminu, для беззнаковых чисел, выдающая минимум из двух чисел. У нас же при вычитании sub EAX,7 может получиться отрицательное число (если ESI<7 после сложения), а отрицательные числа имеют 1 в старшем бите и уж точно больше (беззнаково!) любого положительного числа, даже удвоенного модуля (в данном случае 14). Т.е. команду cmovnc ESI,EAX можно заменить на команду pminu ESI,EAX (не прямо такую, но очень похожую). Если в ESI число больше или равно 7, то EAX положителен и меньше ESI (он же на 7 меньше) и тогда надо взять именно EAX (минимум), если же EAX<0 (и старший бит 1), то ESI<7 и в беззнаковой форме уже ESI меньше беззнакового EAX и команда минимума выдаст именно ESI.

Зачем менять одну команду на другую? Так в AVX нет команды cmov (pblendvb только байтовая и вчетверо медленнее pminu, фтопку), зато каждая команда может работать сразу с 16-ю числами (для модулей <32768)! А для модулей до 128 и сразу с 32-я числами! Т.е. одновременно можно вычислять сразу 32 новых остатка по малому простому модулю. Все три (mov в AVX не нужна) команды за 1.5 такта. Сразу 10 (20 для модулей до 128) чисел за такт, в 10 (20) раз быстрее показанного выше кода! И кстати в 100 (200) раз быстрее варианта с div.
А целочисленных делений в SSE/AVX нет. Связываться же с плавающей точкой: 7 тактов на 4 числа в SSE и 14 тактов на 8 чисел в AVX, для чисел одинарной точности, т.е. для целых лишь до примерно 16млн - вообще неактуально. С двойной точностью вдвое меньше чисел и деление вдвое медленнее: 14 тактов на 2 числа в SSE и 28 тактов на 4 числа в AVX, для целых чисел примерно до 9e15 - ещё более неактуально.

-- 24.01.2024, 02:08 --

Ещё одно огромное преимущество данного подхода: делить длинное/большое исходное число на модуль надо лишь один раз при инициализации. И сколько это займёт времени не так важно, конечно если потом нам надо не тысячи итераций, а сильно поболее (ради тысячи проще на чём угодно посчитать, хоть на калькуляторе), миллиарды и триллионы.
А раз делить нам внутри циклов нигде не надо, и вообще нигде при проверке на простоту не появляются исходные длинные/большие числа и все вычисления идут лишь по модулю небольших простых, то и в каком диапазоне работает проверка в принципе вообще не важно! Хоть около 4млрд, хоть около 97e9999, главное правильно всё инициализировать и потом спокойно считать миллиарды итераций, с одинаковой скоростью. Фактически счёт в описанном варианте идёт просто по модулю 210 (и кроме шага 30 все числа никогда не превышают 7*2=14), а уж где именно в натуральном ряду расположен этот кусок совершенно не важно (для вычислений остатков на следующей итерации из известных на текущей).

Да, ещё. Вычисление x=x+30 из цикла специально убрал: пока не нашли решение он нам и не нужен, работаем только с остатками, а когда нашли - вот тогда и посчитаем, например как z=11+30*(i-1) или z=x+m*(i-1).

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение24.01.2024, 14:38 
Заслуженный участник


20/08/14
11177
Россия, Москва
Где-то выше рассуждал что mov ECX,0 понятнее чем xor ECX,ECX и вне циклов лучше использовать первую.
Сейчас вспомнил про один существенный плюс как раз xor r,r - на современных процессорах она тоже может оказаться бесплатной (не занимающей ни такта), как и mov r,r1. А mov r,0 - нет, такт занимает. Потому внутри нагруженных циклов xor однозначно лучше. Но продолжаю считать что только там, в остальных случаях лучше писать понятнее.

Какие процессоры считать современными? Ну вот в Westmere обе команды, и mov r,r1 и xor r,r, платные (по такту). В Sandy Bridge бесплатная (не занимающая времени) только xor r,r (что очень странно, ожидал наоборот mov). И лишь начиная с Ivy Bridge они бесплатны обе. Для процессоров AMD я не смотрел.
Информация по инструменту Intel® Architecture Code Analyzer версии 2.1 (сокращённо IACA 2.1, последней поддерживающей архитектуры старее Haswell и 32-битный код), измерения сайта uops.info им противоречат и говорят обе бесплатны для всех указанных архитектур. Фог про бесплатность xor вообще не сказал, только про mov. Кому верить? Никому - только реальным тестам.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение24.01.2024, 20:29 
Заслуженный участник


20/08/14
11177
Россия, Москва
Собственно дальше в задаче поиска цепочек простых по паттерну надо или погружаться в AVX или в глубину процессора для тонкой оптимизации (предсказатель переходов, вычислительный конвейер, порты запуска и суперскалярность, внеочередное выполнение, размер кэша L2, скорость памяти, но на всё это у меня готовых примеров нет). Без этого остаётся только оптимизация математики, как в общем и вторым сообщением выше, про асм там совсем чуть-чуть. Не хотелось бы сваливаться в математику.

При этом по прежнему за кадром остались некоторые интересные фишки асма и команд процессора: "склеивание" нескольких команд деления в одну при проверке делимости; проверка делимости умножениями; тест простоты Ферма и используемое им быстрое возведение в степень по модулю; поиск в массивах; вся плавающая точка (и больше не рекомендуемый к использованию FPU как более точный её вычислитель); использование плавающей точки для целочисленных вычислений; целочисленная длинная арифметика; ...

В качестве примера длинной арифметики (правда не двоичной, а десятичной, что само по себе интересно, по цифре в байте, такой выбор будет обоснован) могу взять забавную задачу чисел Лишрел - лет 12 назад занимался ей. Конечно до миллиарда цифр мы не досчитаем, но до сотни тысяч (это порядка десятка миллиардов итераций самого внутреннего цикла) - легко, без всяких MMX/SSE/AVX. Заодно несколько хитростей можно будет посмотреть.

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

Либо можно сделать ещё шажок и научиться через КТО (китайскую теорему об остатках) вычислять прямоугольную матрицу остатков по большому модулю по двум векторам остатков по меньшим модулям. Благо только что как раз и научились почти её считать без делений. Но в программе поиска цепочек простых оно не требуется (тем более именно на асме), можно проще и при том не сильно медленнее.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение24.01.2024, 21:27 
Аватара пользователя


29/04/13
7227
Богородский
Dmitriy40, лично я никак не въеду толком в математику той задачи. Раз уж начал, то попробую разобраться получше. И только потом уже надо будет вернуться к Асму.

Спасибо огромное.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение25.01.2024, 00:20 
Заслуженный участник
Аватара пользователя


21/11/12
1881
Санкт-Петербург
Yadryara
Я, простите, не в теме. Если речь о КТО, может пригодится сервис с mathhelpplanet с пошаговым разбором. Но как ускорить подобные процессы, не представляю.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение25.01.2024, 01:42 
Заслуженный участник


20/08/14
11177
Россия, Москва
Yadryara
Я в математику задачи особо глубоко и не копал, кое-как сделал расчёт таблиц (вместо КТО у меня до сих пор гораздо более прямой и медленный подсчёт). Ну а когда есть таблица (6.636e6 элементов для 37# из поиска 19-252), то дальше ускорять её проверку это уже чисто асм - отказ от делений (требует расширения вектора длиной 6.636e6 в матрицу с 6.636e6 строк по 40 ячеек в каждой), отказ от условных операторов, векторизация (AVX/SSE), экономия трафика памяти (в многопоточном режиме), выбор формата матрицы под имеющиеся команды процессора, и под порядок обхода матрицы (который оказывается выгодно менять по мере продвижения), замена кучи сравнений на векторную косвенную выборку из регистра, доработка её от байтовой до битовой (иначе и не применить).
Но да, математику лучше в той теме. Правда с математикой стоит всегда держать в уме что надо в итоге получить (не просто вектор, он то не такая проблема, а матрицу). Либо придёшь к совсем другой программе (а может и алгоритму).


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

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение28.01.2024, 00:12 
Заслуженный участник


12/07/07
4448
Ветвления 1 (CPU)

При программировании 86 (а затем 186, 286, и в меньшей степени 386,….) большую роль играло то, что многие инструкции (например sub, add, dec, inc, and, xor,...) изменяли флаги в соответствии со своим результатом. В простейших случаях используются флаги:
CF (Carry F, флаг переноса) — устанавливается в 1, если операция вызвала перенос (при сложении) или заём (при вычитании), иначе 0;
PF (Parity F, флаг чётности) — устанавливается в 1 , если младший байт содержит чётное число единиц, иначе 0;
AF (Auxiliary carry F, флаг вспомогательного переноса) — устанавливается в 1, если арифметическая операция вызвала перенос (при сложении) или заём (при вычитании) из младшей тетрады (т.е бита 3) результата (используется в десятичной арифметике, программно недоступен);
ZF (Zero F) — устанавливается в 1, если результат 0;
SF (Sign F, флаг знака) — устанавливается в 1, если старший бит результата равен 1, иначе 0;
OF (Overflow F, флаг переполнения) — устанавливается в 1, если в арифметических операциях результат превышает диапазон представления чисел.

Например,
add, sub, dec, inc устанавливают OF, SF, ZF, AF, PF (add, sub и CF);
and, or, xor — SF, ZF, PF, а OF и CF сбрасывают в ноль («межразрядные связи отсутствуют»).

Значение флага используют инструкции условного перехода. Мнемоники имеют вид jсс (jamp «по условию сс»), где сс — одна или больше букв). Переход выполняется на инструкцию помеченную меткой. Переходы делятся на короткие (short), ближние (near) и дальние (far). Короткие переходы — это переходы на инструкцию отстающую от jx на +127/-128 байт. Если компилятор однопроходный, то для переходов назад он может самостоятельно определить типа перехода (короткий/ближний). В случае перехода вперёд имеются ключевые слова, поясняющие компилятору дальность перехода. В некоторых компиляторах в случае отсутствия указания внутрисегментные переходы кодируются как близкий, но по результату трансляции выдаётся предупреждение, а пользователь может указать в нужном месте вид перехода.

Примеры инструкций условного перехода:
jc / jnc — перейти, если CF поднят / не поднят;
js / jns — перейти, если SF поднят / не поднят;
jo / jno — перейти, если OF поднят / не поднят;
je или jz / jne или jnz — если ZF поднят / не поднят.

Есть инструкции условных переходов анализирующие несколько флагов.

Между инструкцией, результат которой определяет ветвление, и условным переходом могут идти (по алгоритму) другие инструкции, которые могут изменить информативный флаг.
Имеются инструкции jcxz/ jcxz (переход, если cx/ ecx содержит 0). (Банальный пример: обойти участок кода (например, цикл), если cx/ ecx содержит 0.) Это двухбайтовая инструкция — переход только короткий!
Также имеются инструкции (386 и выше) «сохранения условия»
setСС r8/m8
Если проверяемое условие удовлетворяется (в регистре флагов соответствующие флаги подняты/опущены), в регистр/память помещается 1, если не удовлетворяется, то 0.
Условия СС те же, что и для jcc. Например, setc / setnc, setz (или sete)/ setnz (или setne), sets/ setns.

Кроме инструкций условного перехода значения флагов могут использоваться инструкциями «условные mov»
cmovCC (процессоры P6 и выше, но не у всех есть поддержка этих инструкций).



Наиболее часто используется условие Z (E). Рассмотрим первую программу этой ветки: вычисление чисел Фибоначчи, помещающихся в 32/64 битную переменную. Для вычисления n-го числа нужно выполнить n-2 сложений. Уже до начала сложений мы знаем — сколько раз повторится цикл. Поэтому можно занести в некоторый свободный регистр общего назначения (например, ECX) n-2 и выполнять итерации до тех пор пока его значение не станет 0. На каждой итерации уменьшать значение на 1.
Не задумываясь над оптимальностью, мы на скорую руку можем написать:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
format PE
include 'win32axp.inc'

n=48   ;Какое число Фибоначчи вычислить

.code
main:
;Начало вычислений
        mov     EAX,1           ;x=a2
        mov     EDX,1           ;y=a1
        mov     ECX,n           ;n
        sub     ECX, 2
cycle:  xadd    EAX,EDX         ;z=y+x
        jc ovf
        sub     ECX, 1
        jnz     cycle           ;if не равно нулю, то уйти к метке
;Конец вычислений, число в EAX
        cinvoke wsprintf, buf, fmt1, n, EAX
        invoke  MessageBox, 0, buf, txt1, MB_OK + MB_ICONINFORMATION
        invoke  ExitProcess,0

ovf:    invoke  MessageBox, 0, err1, txt1, MB_OK + MB_ICONSTOP
        invoke  ExitProcess,199
err1:   db      'Overflow!',13,10,0

fmt1:   db      'a(%d)=%u',13,10,0
txt1:   db      'Fibonacci number',0

.data
buf:    rb      1000

.end main


Если абстрагироваться от cmp в первоначальном варианте программы, то сложение и обмен регистрами add, mov r, r здесь заменёны на «сложить и обменять регистры» xadd.

Что будет быстрее обмен содержимого регистров при помощи mov или xadd или расширение цикла c выходом вперёд как в примере c SSE, но без SSE (и cmp, конечно), а c add. Вариант с FPU совсем не актуальный, но в нём использована инструкция обмена регистров, что даёт повод поговорить о переименовании регистров.
Циклы очень короткие с очень простыми телами, протестировать эти циклы крайне трудно. Возможно уже пришло время поговорить об архитектуре?

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение28.01.2024, 01:22 
Заслуженный участник


20/08/14
11177
Россия, Москва
Насчёт архитектуры повторю своё мнение: пока не касаемся оптимизации программы (по скорости) и векторизации, программисту достаточно знать про 8 (16 под x64) РОН, регистр флагов и EIP. И про специальные функции ESP и EBP (второй только при пользовании встроенными методами компилятора ассемблера). Плюс соглашение о передаче параметров под используемой ОС. Всё остальное - только когда возникнет реальная потребность в оптимизации. Специальные функции остальных регистров узнавать по ходу дела, конкретно по командам, слишком там всё заморочено.
FPU стоит особняком, очень уж оно специфическое. И считаю стоит изучать лишь если есть реальная потребность в его возможностях, не покрываемая SSE (в скалярном режиме).

Цикл под x32 не сможет выполняться более примерно 4млрд раз (без заморочек с длинной арифметикой, что ну совсем редкость), даже с одним делением в цикле это максимум миллиардов 50 тактов, или меньше минуты. Ну и что тут оптимизировать ... "Всё что работает лишь несколько минут (а то и пару часов) - в оптимизации не нуждается", ИМХО.
А если в цикле окажутся долгие команды или их будет много и цикл станет занимать десятки тактов на итерацию, то тем более экономия 1-2 тактов на организацию цикла роли не сыграет.
Потому считаю оптимизировать организацию и слишком простых циклов и слишком сложных - одинаково бессмысленно, они или и так достаточно быстрые, или оптимизация не даст заметного эффекта, а гнаться за долями процентов скорости ... вы меня простите.
Оптимизировать надо вычисления, а не пару команд организации циклов.

Впрочем это всё в расчёте именно на практику, разумеется теория ещё никому не мешала. Но это другая цель. Тоже прекрасная и я обеими руками "за" чтобы тотально все знали устройство своего проца назубок :mrgreen:, но ... реально это мало кому нужно. А когда понадобится - тогда и изучат, по мере необходимости и в разумном объёме.

Но я и не против других желающих что-то рассказать.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение28.01.2024, 12:12 
Заслуженный участник


20/08/14
11177
Россия, Москва

(Оффтоп)

Стал внимательнее анализировать внутренние циклы в своей программе (для реализации идеи про вычисление КТО из темы о паттернах/кортежах), обнаружил забавную ситуацию: вычисление суммирования по модулю с последующей проверкой суммы по списку разрешённых/запрещённых остатков для матрицы 32 числа по 12 простым (т.е. 32*12=384 суммы по модулю с проверкой) длиной 200 мопов занимает 40 тактов (анализатор говорит 43-46, но загруженность портов не превышает 37, до реальных измерений никак не доберусь, потому пока беру 40), в среднем по 0.1 такта на сумму с проверкой или 1.25 такта на одно число с проверкой по всем простым, а вычисление ровно этого же для одного числа по следующим 23 простым (1*23=23 суммы) занимает 35 тактов, по 1.5 такта на сумму с проверкой.
Охренеть. Вот такая разница между Throughput (первые суммы и проверки выполняются параллельно) и Latency (вторые суммы и проверки выполняются практически последовательно). Хотя и то и другое активно использует AVX, но если первое заняло практически все вычислительные порты, то второе выполняется почти в одном порту (но разном для каждой команды).
В итоге обработка 32-х чисел по всем простым занимает 110 тактов или 3.5 такта на число. Что должно давать до 1e9 чисел (в данном случае паттернов/цепочек) в секунду в один поток. Теоретически. Реальная же скорость 600e6, в полтора раза ниже. Даже интересно почему ... Потому и заморочился анализом циклов, вдруг там ещё есть что ускорять. ;-) И кое-что нашёл, может поэтому и теоретическая скорость поднялась, надо дописать и померить реальную.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение02.02.2024, 16:29 
Заслуженный участник


20/08/14
11177
Россия, Москва
Смотрю интереса ни у кого так и не появилось. А желающих что-то рассказать сам распугал.
Тогда пока выскажу крамольную для данной темы мысль: чтобы пользоваться ускорением от векторизации (SSE/AVX) вычислений совсем не обязательно изучать ассемблер. Можно изучить С (который похож на известным тут многим PARI/GP) и ещё всего две вещи: векторные структуры данных и векторные команды (что с какими данными они делают). Первая совсем простая, там лишь байты (8 бит), слова (16 бит), двойные слова (32 бит), четверные слова (64 бит). 128 бит встречается крайне редко, буквально в нескольких командах. И два плавающих типа, single (32 бит) и double (64 бит).
А вот команд много, да, больше сотни пожалуй. Но основных около десятка типов: плюс/минус, сравнения, умножение, деление, сдвиги, логика, перестановки, упаковка/распаковка, преобразования типов, несколько спецкоманд (мин/макс, горизонтальные, математика).
Про процессор и ассемблер тут почти ничего знать не надо, для начала достаточно помнить что скорость команд выражается соотношением $\operatorname{div} \ll \operatorname{mul} < \operatorname{others}$. Т.е. деление самое медленное, умножение лишь немногим медленнее почти всех прочих команд. По мере накопления опыта можно будет уточнять скорость каждой команды.

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

Речь про инстриксы для C от Intel (из РФ через VPN). Там же и их полное описание, это как раз и есть те команды что надо бы знать.

Реально это выглядит так. Возьму кусок кода из показанного выше:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
.l32:           vmovdqu         ymm6,[mask128]          ;23     3/0.5   mask128
                vmovdqu         ymm0,[EDI+ECX]          ;23     3/0.5   a
                vpaddb          ymm0,ymm0,[ESI]         ;15     1/0.5   b
                vpsubb          ymm1,ymm0,[EDI+const]   ;15     1/0.5   p
                vpminub         ymm0,ymm0,ymm1          ;15     1/0.5
                vpand           ymm1,ymm0,[mask78]      ;015    1/0.33  маска 0x78
                vpsrlw          ymm1,ymm1,3             ;0      1/1
                vpshufb         ymm0,ymm7,ymm0          ;5      1/1
                vpshufb         ymm1,ymm6,ymm1          ;5      1/1
                vpand           ymm0,ymm0,ymm1          ;015    1/0.33  биты допустимости каждого из байтов
                vpcmpeqb        ymm0,ymm0,ymm5          ;15     1/0.5   маска 0x00, теперь биты превратились в байты (и инвертировались)
                vpmovmskb       EAX,ymm0                ;0      3/1
                not             EAX                     ;0156   1/0.25
                and             EBX,EAX                 ;0156   1/0.25
                jz              .next                   ;06     1/0.5
                add             ESI,const               ;0156   1/0.25
                add             EDI,const               ;0156   1/0.25
                cmp             EDI,const               ;0156   1/0.25
                jc              .l32                    ;06     1/0.5
На С он будет выглядеть примерно так (примерно! не проверял! с указателями идентичности явно нет, но смысл и не в них):
Используется синтаксис C
void f(uint8_t n, __m256i *a, __m256i *b, __m256i *p, __m256i mask128) {
        const __m256i m00 = _mm256_setzero_si256();
        const __m256i m78 = _mm256_set1_epi8(0x78);
        const __m256i bits = _mm256_set1_epi64x(0x8040201008040201);
        __m256i x;
        uint32_t ebx = -1;
//Сам показанный выше цикл
        for(uint i=0; (i<n) && (ebx!=0); ++i) {
                x = _mm256_add_epi8(*a++, *b++);
                x = _mm256_min_epu8(x, _mm256_sub_epi8(x, *p++));//x=(a+b)%p
                x = _mm256_and_si256(_mm256_shuffle_epi8(mask128, _mm256_srli_epi16(_mm256_and_si256(x, m78), 3)), _mm256_shuffle_epi8(bits, x));
                ebx &= ~(_mm256_movemask_epi8(_mm256_cmpeq_epi8(x, m00)));
        }
}
Разумеется это лишь внутренний цикл, дальше будет анализ ebx (32 бита допустимости каждого их 32 чисел), потом дополнительные проверки, потом обрамляющий цикл по всей таблице вот этими кусками по 32 числа, но для иллюстрации вполне достаточно.
По моему вполне понятно. Если знать что это за _mm256_xxx функции и тип данных __mm256i.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение06.02.2024, 19:33 
Заслуженный участник


20/08/14
11177
Россия, Москва
Наткнулся на более удобный список SSE/AVX команд: https://www.officedaytime.com/simd512e/
По каждой команде есть наглядное пояснение что и как она делает.
Интринсики тоже приведены.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение05.03.2024, 20:04 
Заслуженный участник


20/08/14
11177
Россия, Москва
Выступлю как оппонент себе же: выше уже был аргумент против изучения именно языка ассемблера (достаточно интринсов с таблицами тактов и типов данных), добавлю ещё один.

(Оффтоп)

Понадобилось мне тут поменять одну из программ, внутренний цикл в которой на асме, а внешние и подготовка таблиц на ЯВУ, и уже больше месяца работа не идёт дальше обдумывания мелких частностей и буквально нескольких строчек кода. Потому что оказалось переписывать надо почти всё, и внутренний цикл (что сделал почти сразу), и почти все внешние, и плюс добавлять между ними ещё две прослойки/процедуры подготовки, и всё это завязано друг на друга столь сильно, что например изменение длины любого цикла рушит все структуры данных, каждый раз приходится буквально заново их все продумывать, просчитывать (что куда как раскидать), проверять соблюдение сразу многих условий (взаимно противоречивых и потому постоянный поиск компромисса) - и каждый раз существенно переписывать код, ведь меняются структуры данных, порядки их обхода, разделение на внутренний и внешние циклы, длины структур, ожидаемая вероятность длины циклов (от которой зависит например куда включать проверку на досрочный выход, а куда не включать ради скорости), и многое другое. На нормальном (ООП) ЯВУ это решалось бы типизацией, наследованием операций, итераторами без опоры на тип итерируемых структур, а на асме приходится для каждого варианта чуть ли не заново продумывать и распределение регистров, и методы хранения индексов внутренних циклов, и методы адресации во внутренних циклах (они заметно влияют на скорость), и конкретные последовательности команд процессора (многие действия можно выполнить более чем одним способом, но почти всегда нет одного явно предпочтительного, у каждого свои достоинства и недостатки, и приходится комбинировать их в соседних кусках кода для выпячивания первых и нивелирования вторых). Это всё конечно безумно интересно, но очень муторно и занимает страшно много времени и радует пожалуй лишь фанатов именно такого времяпрепровождения. И хоть всё вроде и понятно что и как надо сделать, но конечного результата пока так и нет - а он практически важен, даст примерно пятикратное ускорение вычислений, которые идут уже год и займут ещё может и несколько лет, потому ускорить критично. И самое неприятное, что не получается оценить конечный эффект без написания практически 90% уже правильного кода - все промежуточные тесты (а их тоже пишется немало) слишком искусственны и могут ошибаться в разы по скорости, надо мерить именно на боевой конфигурации. На ЯВУ это было бы заметно проще и быстрее в написании. Но и заметно медленнее в работе, а потому смысла не имеет. А переписывание с ЯВУ на асм отдельных кусков - не срабатывает, все структуры данных приходится адаптировать под имеющие аппаратные команды (которых до полдесятка вариантов), а соответственно переписывать и всё выше по их генерации и обработке, и это тоже попадает на уровень заметного влияния на скорость и тоже приходится писать на асме (грубо говоря из пяти вложенных циклов на ЯВУ можно сделать лишь один верхний, в остальных слишком много накладных расходов).
Такая вот засада с написанием оптимизированного ассемблерного кода. Слишком много деталей надо предусматривать и держать в памяти. И слишком много кода переписывать при любом изменении алгоритма или даже мелких деталей его реализации. И это вполне типичная ситуация (при оптимизации по скорости).

 Профиль  
                  
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ Сообщений: 133 ]  На страницу Пред.  1 ... 5, 6, 7, 8, 9

Модераторы: Karan, Toucan, PAV, maxal, Супермодераторы



Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group