2014 dxdy logo

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

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




Начать новую тему Ответить на тему На страницу Пред.  1, 2, 3, 4, 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
Сообщение18.01.2024, 11:29 
Заслуженный участник


12/07/07
4530
worm2 в сообщении #1626331 писал(а):
для 8-битных значений — byte ptr
для 16-битных значений — word ptr <...>
От языка зависит. Например, в fasm просто byte, word, ... (без ptr).
worm2 в сообщении #1626331 писал(а):
в то время как для беззнаковых используются другие инструкции (MOVZX либо явное обнуление старших разрядов инструкцией AND)
Вот это место желательно изложить подробнее. И почему and? В каком контексте? Может xor r, r? («Не исполняемая инструкция» в современных процессорах, идиома очистки). В общем, на мой взгляд, это место желательно написать минимально подробней, хоть как-то обосновывая архитектурой (устройством процессора).

(Если это заготовка для будущей темы форума или учебника/конспекта/методички.)

-- Thu 18.01.2024 11:12:50 --

Dmitriy40 в сообщении #1626317 писал(а):
И кстати я не согласен с такой вот оптимизацией, вне циклов лучше (точно понятнее) писать прямо mov edx,0, пусть и чуть больше размер программы. И даже в циклах это очень под вопросом, будет ли что-то оптимальнее или нет,
Intel® 64 and IA-32 Architectures Optimization Reference Manual, Order Number: 248966-033 June 2016 писал(а):
3.5.1.8 Clearing Registers and Dependency Breaking Idioms
Code sequences that modifies partial register can experience some delay in its dependency chain, but can be avoided by using dependency breaking idioms. In processors based on Intel Core microarchitecture, a number of instructions can help clear execution dependency when software uses these instruction to clear register content to zero. The instructions
include:
XOR REG, REG
SUB REG, REG
XORPS/PD XMMREG, XMMREG
PXOR XMMREG, XMMREG
SUBPS/PD XMMREG, XMMREG
PSUBB/W/D/Q XMMREG, XMMREG
<..>
Assembly/Compiler Coding Rule 36. (M impact, ML generality) Use dependency-breaking-idiom instructions to set a register to 0, or to break a false dependence chain resulting from re-use of registers. In contexts where the condition codes must be preserved, move 0 into the register instead. This requires more code space than using XOR and SUB, but avoids setting the condition codes.
xor r, r для очистки используется со времён до 8086. Это как раз пример интуитивно понятного кода. Даже если он ничего не даёт, то так чистят рег.

Intel® 64 and IA-32 Architectures Optimization Reference Manual, Order Number: 248966-033
June 2016
писал(а):
3.5.1.1 Use of the INC and DEC Instructions
The INC and DEC instructions modify only a subset of the bits in the flag register. This creates a dependence on all previous writes of the flag register. This is especially problematic when these instructions are on the critical path because they are used to change an address for a load on which many other instructions depend.
Assembly/Compiler Coding Rule 33. (M impact, H generality) INC and DEC instructions should be replaced with ADD or SUB instructions, because ADD and SUB overwrite all flags, whereas INC and DEC do not, therefore creating false dependencies on earlier instructions that set the flags.
В эпоху 8086/80186/80286 add r, 2 заменяли (в подходящих случаях) двумя inc, и комбинация dec-loop была идиомой. С 80386 ситуация изменилась: (грубо говоря) dec-loop заменено на sub jx. Если архитектура точно не указана, то это — стандартный подход. Это не стопроцентное правило и просто протестить не получится. В последних Architectures Optimization Reference Manual dec/inc «не изгоняются» (или устали повторять, или немного починили).

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


20/08/14
11867
Россия, Москва
Тогда уж надо упомянуть и про обнуление старшей половины регистров Rxx при записи в младшую Exx/RxxD (но только именно всего 32 двойного слова) под x64. Это удобно и важно. Позволяет почти произвольно смешивать x32 и x64 код (с беззнаковыми числами) под x64 ОС.

Yadryara в сообщении #1626354 писал(а):
Вот на эти два метода мне ещё рано смотреть?
Да в принципе уже можно. Как и сразу на следующее улучшение -тест Ферма и его улучшение до сильных псевдопростых (это важная веха, практически полезная). Только как-то сразу много новых понятий придётся ввести: и функции, и передачу параметров, и переменные в памяти, и массивы, и соответственно методы адресации (некоторые были показаны worm2, но без пояснений), ну и команды умножения/деления.
А замену делений на умножения просто хочу показать как красивую идею оптимизации. Как и совмещение нескольких делений в одно (это особой пользы уже не имеет, но тоже считаю красиво).
Но меня очень раздражает что для смены чисел приходится пока что перекомпилировать программу, я привык что числа можно задавать с клавиатуры или из файла или (удобнее всего) в командной строке при запуске, а программа скомпилена один раз и всё. Но если это не интересно, хорошо, давайте дам упрощённый аналог функции scanf из C (в WinAPI её готовой я не нашёл и пришлось писать самому) из второго пункта плана без пояснений (если что зададите вопросы). Это мегаполезно для управления работой программ.
ОК, тогда сейчас к вечеру мск набросаю примеры под x32, их получится штуки 3-4, только метод делений и его три улучшения.

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


29/04/13
8307
Богородский
Dmitriy40 в сообщении #1626376 писал(а):
ОК, тогда сейчас к вечеру мск набросаю примеры под x32, их получится штуки 3-4, только метод делений и его три улучшения.

Вы, главное, не торопитесь. Я уже настроился что это долгий путь.

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


20/08/14
11867
Россия, Москва
GAA в сообщении #1626365 писал(а):
Это как раз пример интуитивно понятного кода.
Не согласен: это интуитивно понятно только если хорошо знаком с логическими операциями. Многие не пропустили мимо ушей тему про логические операции (как основу АЛУ) и напрактиковались с ними? Если не для экзаменов? По моему это как раз из серии "дают непонятно что непонятно зачем". Кому было интересно само по себе (как мне) тот и разобрался, остальные ... Им понятнее sub r,r. И ещё более понятно mov r,0. А по скорости (которая только и волнует) они все одинаковы.

На оптимизацию же по размеру кода я откровенно плюю в данной теме (хотите коротких программ - используйте Форт или аналогичные языки с косвенным кодированием всех функций и без кода передачи параметров), тут про оптимизацию по скорости. Когда-нибудь упомяну (как раз с такими примерами) что можно и по другим критериям оптимизировать, но это только уже когда будет именно про оптимизацию. Ну и оптимизировать единичные команды с длиной в пяток байтов при наличии кучи AVX команд длиной до десятка байтов каждая - не считаю разумным (есть неформальное понятие переоптимизации, вот это оно), всё равно кэш мопов (правда только с Sandy Bridge) на размер команды завязан слабо, а размера L1I на кэш мопов хватит по любому (трюки же эпохи 80386 остались лишь в истории). Но даже для архитектур Nehalem и Core покажите мне оптимизированный вручную внутренний цикл на асме размером под 30КБ! :facepalm: Это потянет на медальку.

И соответственно ещё раз повторю: старые процессоры не интересны. Всё что P4 и старее (т.е. до архитектуры Core или даже конкретно Nehalem) интересны лишь исторически. На практике оптимизировать код под них уже давно не нужно. Тема создана по реально практическому поводу, превращать её в полноценный курс по всем разновидностям процессоров я не планирую и не смогу (например у меня нет ни одного процессора AMD). Хотите - помогайте. Но тогда попрошу в контексте, сейчас ссылаться на столь тонкие вопросы оптимизации слишком рано. И да, в современных архитектурах inc и add выполняются с одинаковой скоростью. А все эти loop и enter/leave и строковые команды и прочие придумки эпохи 8086 - намного медленнее.

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


01/08/06
3136
Уфа
GAA в сообщении #1626365 писал(а):
worm2 в сообщении #1626331 писал(а):
в то время как для беззнаковых используются другие инструкции (MOVZX либо явное обнуление старших разрядов инструкцией AND)
Вот это место желательно изложить подробнее. И почему and? В каком контексте? Может xor r, r? («Не исполняемая инструкция» в современных процессорах, идиома очистки). В общем, на мой взгляд, это место желательно написать минимально подробней, хоть как-то обосновывая архитектурой (устройством процессора).
Для беззнаковых целых чисел понятно, что расширение их битности требует размещения нулей во всех новых (старших) битах (точно так же, как в десятичной системе 15 = 015 = 0015...).
Кстати, упомяну о том, что новое расширенное значение можно интерпретировать и как знаковое, значение будет правильным: 16-битное 00FFh будет представлять то же самое число 255 независимо от того, знаковое это число или беззнаковое: знаковые и беззнаковые числа кодируются одинаково в пересечении их диапазонов (там где старший бит нулевой). Это позволяет легко преобразовывать беззнаковые типы в более широкие знаковые, что широко используется в ЯВУ. Но не наоборот: отрицательные значения не могут быть представлены сколь угодно широким беззнаковым типом, ибо все беззнаковые типы кодируют только неотрицательные числа.
Итак, когда речь идёт про значения в оперативной памяти, расширение беззнакового типа эквивалентно просто размещению нулей в нужном количестве старших байтов.
Например, расширение 8-битного беззнакового числа до 32-битного требует обнуления трёх байтов. Проще всего это сделать единообразно инструкцией AND, поскольку инструкция MOV может записывать только 1, 2, 4 или 8 байтов. А инструкция XOR здесь не подходит для обнуления, поскольку (как и все другие инструкции в x86) не позволяет указывать оба операнда (и источник, и назначение) в памяти, что-то одно должно быть регистром или непосредственным значением.
Побитовая операция AND, напоминаю, выдаёт бит 1 только в том случае, если оба входных бита = 1. В случае, если хотя бы один бит нулевой, AND выдаёт 0. Инструкция AND, как и в ЯВУ, осуществляет побитовую операцию AND над всеми битами операндов и помещает результат в первый операнд.
Небольшое отступление: это общее правило языка ассемблера x86 в синтаксисе Intel (который мы тут только и признаём): любая бинарная операция # (C = A # B) всегда в качестве результата имеет один из входных операндов, т.е. имеет вид A = A # B, причём A указывается на первом месте, а B — на втором, после запятой. Мы не можем одной инструкцией сложить, например, RAX с RCX и результат поместить в RDX (хех, ну да, можем на самом деле, но это такой фокус, трюк, который мы здесь пока игнорируем).
Поэтому операция AND для расширения беззнакового числа в памяти выглядит так:
Используется синтаксис ASM
        and       [<память>], <маска>

где <маска> — это непосредственное значение той же разрядности, что и результат (расширенное до нужной битности), у которой все биты, соответствующие расширяемому числу, установлены в 1 (согласно логике оператора AND, соответствующие биты в памяти останутся неизменными, т.к. 1 AND X = X), а более старшие биты обнулены (соответствующие биты в памяти будут обнулены, т.к. 0 AND X = 0).
Например, расширение беззнакового 16-битного значения до 64-битного:
Используется синтаксис ASM
        and       qword [<память>], 0FFFFh   ; или "qword ptr"

Поскольку здесь у нас упор на скорость, то я должен упомянуть о том, что инструкции такого вида и читают из памяти, и пишут в неё. Это может быть медленнее инструкции MOV [память], 0. Может быть, медленнее даже двух таких инструкций, требуемых, чтобы записать 3 или 6 байтов. Так что можно отдать должное обнулению через MOV, если мы, так сказать, идём на рекорд, может быть, оно и будет быстрее.
Если мы говорим про операции над регистрами, а не в памяти, то я должен рассказать о регистрах процессора кое-что, здесь ещё не упомянутое.
В 32-битном режиме регистры 32-битные, а в 64-битном — 64-битные, однако все 32-битные регистры в 64-битном режиме также доступны, называются так же, и являются младшими разрядами более широких вариантов:
У 64-битного регистра RAX младшие 32 бита — это регистр EAX
Этот праздник продолжается дальше:
У 32-битного регистра EAX младшие 16 бит — это регистр AX
У 16-битного регистра AX младшие 8 бит — это регистр AL, а старшие 8 бит — это регистр AH.
Вышенаписанное распространяется на регистры RBX, RCX, RDX, которые являются, так сказать, главными арифметическими регистрами, хотя с некоторыми ограничениями это справедливо и для остальных регистров общего назначения (RSI, RDI, RBP, R8-R15).
Таким образом, чтобы расширить беззнаковый 8-битный регистр на 16 бит, достаточно обнулить его старший именованный регистр:
Используется синтаксис ASM
        mov       BH, 0      ; преобразовать 8-битное значение BL в 16-битное BX
или, если хотите:
Используется синтаксис ASM
        xor       BH, BH

Однако для расширения 16->32, ?->32, ?->64 такой приём уже не подходит, поскольку старшие 16- и 32-битные "половинки" регистров недоступны как самостоятельные регистры в архитектуре x86, поэтому приходится использовать ту же инструкцию AND:
Используется синтаксис ASM
        and       EDX, 0FFFFh   ; преобразовать 16-битное значение DX в 32-битное EDX


А теперь забудьте о том, что я здесь написал :D Потому что у процессора есть специальные инструкции для расширения чисел, как беззнаковых, так и знаковых:
Используется синтаксис ASM
        MOVZX   <широкий регистр>, <узкий регистр или память>

расширяет беззнаковые значения (обнуляет старшие биты);
Используется синтаксис ASM
        MOVSX   <широкий регистр>, <узкий регистр или память>

расширяет знаковые значения (копирует знаковый бит во все старшие разряды). Например:
Используется синтаксис ASM
        MOVZX   AX, BL           ; 8 бит без знака -> 16 бита
        MOVSX   EDX, word [EAX]  ; 16 бит со знаком -> 32 бита

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

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


20/08/14
11867
Россия, Москва
Противореча сам себе (вот такой я ветренный) покажу практически полезный момент разницы между add и inc (что последняя не портит перенос):
Используется синтаксис ASM
        mov     EBX,0           ;смещение текущего обрабатываемого двойного слова
        mov     ECX,100         ;длина слагаемых в двойных словах
        clc                     ;сначала переноса нет
.sum:   mov     EAX,[num1+EBX*4]
        adc     EAX,[num2+EBX*4];сложение с переносом от младшего двойного слова
        mov     [res+EBX*4],EAX ;res[]=num1[]+num2[]
        inc     EBX             ;продвинем все три указателя
        dec     ECX             ;одно двойное слово обработали
        jnz     .sum
Тут перенос сохраняет своё правильное значение между итерациями цикла и ни inc ни dec его не затрагивают и не портят, в отличие от add/sub.
Заодно тут видно что к памяти можно адресоваться по сумме константы (адреса/указателя на массив) и регистра, причём его можно ещё и домножить на 2,4,8. Можно и ещё регистр прибавлять, без домножения. Или без константы.
Ну и флаг переноса можно сбросить специальной командой clc (а установить командой stc и проинвертировать командой cmc).
То что метка с точки - это полезная фича FASM, такие метки локальны для каждой процедуры (точнее между двумя глобальными метками, которые без точки). Удобно, можно в каждой функции иметь метку .sum или .next или .exit и не придумывать уникальное имя для каждой функции. Про ещё одну фичу FASM с меткой @@ не говорю специально - считаю метки должны иметь понятные имена и исключений из этого правила достаточно мало и как-нибудь потом.

Если кто не догадался то это суммирование двух длинных чисел в дополнительном коде (т.е. можно и отрицательных), тут каждое по 3200 битов.
Сильно подозреваю что такое странное поведение inc/dec (не трогать перенос) как раз для этого и сделали.
Вот только применение этой фичи фактически таким кодом и ограничивается (ну ещё длинные сдвиги есть, но там давно сдвиг лучше делать через другую команду, без переноса).

-- 18.01.2024, 14:36 --

worm2 в сообщении #1626386 писал(а):
Небольшое отступление: это общее правило языка ассемблера x86 в синтаксисе Intel (который мы тут только и признаём): любая бинарная операция # (C = A # B) всегда в качестве результата имеет один из входных операндов, т.е. имеет вид A = A # B, причём A указывается на первом месте, а B — на втором, после запятой. Мы не можем одной инструкцией сложить, например, RAX с RCX и результат поместить в RDX (хех, ну да, можем на самом деле, но это такой фокус, трюк, который мы здесь пока игнорируем).
Не только мегаудобная lea, но и imul r,r,i и shrd r,r,cl. А с Haswell в 2013 и mulx r,r,r и shrx r,r,r и pdep/pext/bzhi/bextr и даже andn. Так что исключения есть, и немало, и mulx страшно удобная (а shrd вообще однозначно must use). А ещё почти все AVX команды трехадресные (чем и ценны даже для работы с SSE).

Ещё пара замечаний/дополнений.
Про RxxD так и не сказали. Как и про обнуление старшей половины Rxx при записи в младшую Exx/RxxD.
И что слова и байты есть только для xAX, xBX, xCX, xDX, регистры xSI, xDI, xBP, xSP, R8-R15 доступны только целиком (все 32 или 64 бита).
Ну и от себя добавлю что запись в байт/слово любого регистра может сильно замедлить выполнение следующей команды если она будет использовать регистр целиком (32 или 64 бита). Так что mov bh,0 сильно хуже and ebx,0xFF (а movzx ещё лучше). Потому в регистрах почти всегда удобнее работать с полным регистром (32 или 64 бита), а переходить к меньшим объектам только при записи в память.

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


01/08/06
3136
Уфа
Я сам архитектуру x86 слабо знаю. Особенно что касается новых инструкций (MMX и после него). Планировал только про самое основное рассказать, чтобы Вам чуть меньше на это отвлекаться.
Про обнуление старшей половины Rxx при записи в соответствующий Exx знал, но забыл.

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


12/07/07
4530
worm2, спасибо! Стало понятней.
--------------------
На странице Intel® 64 and IA-32 Architectures Software Developer Manuals можно найти документацию по инструкциям, оптимизации и немного таймингам (терминология с Фогом не совпадает) [Это инфа для самых начинающих. Кидать в меня тухлыми яйцами за банальность ссылки не надо].

-- Thu 18.01.2024 14:43:28 --

Ранее тайминги Intel приводил (не для всех инструкций) в Intel® 64 and IA-32 Architectures Optimization Reference Manual.

-- Thu 18.01.2024 14:58:11 --

В конце второго тома 23 года также немного о таймингах есть.

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


20/08/14
11867
Россия, Москва
Dmitriy40 в сообщении #1626387 писал(а):
R8-R15 доступны только целиком (все 32 или 64 бита).
Поправлюсь: для R8-R15 доступна и младшая половина R8D-R15D. Но не слова (16 бит) и байты (8 бит).

GAA в сообщении #1626397 писал(а):
Ранее тайминги Intel приводил (не для всех инструкций) в Intel® 64 and IA-32 Architectures Optimization Reference Manual.
Есть у меня он, 2015г. Посмотрел мельком, не нашёл, не считать же короткую табличку в "2.4.3.1 Issue Ports and Execution Units" за такую инфу, это ни о чём. раздел 14.4 тем более не актуален, Atom тихо умер и никто ничего вычислительного на нём считать не будет. Упс, в новом 2023 всё поменялось, инфы стало сильно больше.
А, долистал до "APPENDIX C INSTRUCTION LATENCY AND THROUGHPUT", да, действительно есть, и подробно. Был не прав, значит всё же публикует, спасибо.
Хотя, без распределения по портам запуска (как у Фога), а это важно для оптимизации.

Yadryara
Но вообще да, если кто хочет хорошо разбираться в оптимизации программ, то читать этот документ очень даже стоит, как минимум первую половину (первые 7 разделов, до многозадачности, и 11-й раздел про AVX, и приложение C), страниц 400 (с примерами). Кое-где мельком, кое-где внимательно. Это чтобы оценить объём знаний, так-то не предлагаю, о многом и так расскажу.

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


01/08/06
3136
Уфа
Dmitriy40 в сообщении #1626400 писал(а):
Поправлюсь: для R8-R15 доступна и младшая половина R8D-R15D. Но не слова (16 бит) и байты (8 бит).

Как интересно! Полез почитать про эти половинки, а вот тут пишут, что, оказывается, младшие 16 и 8 бит для этих регистров тоже доступны (R8W, R8B и т.п.):

https://wiki.osdev.org/CPU_Registers_x86-64

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


20/08/14
11867
Россия, Москва
И снова был неправ, действительно доступны. Как-то не отложилось в памяти. :-(

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


20/08/14
11867
Россия, Москва
Итак, проверка числа на простоту. x32, проц минимум 80386, числа до $2^{32}$, без знака, в консоли.
Нужны 3 файла, prog3.asm с содержимым:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
format PE console
include 'win32axp.inc'

.code
main:
                invoke  GetTickCount            ;Запросим текущее время в мс
                mov     [time0],EAX
                invoke  GetStdHandle,STD_OUTPUT_HANDLE  ;Получить handle консоли вывода
                mov     [hOut],EAX
                invoke  GetStdHandle, STD_ERROR_HANDLE  ;Получить handle консоли ошибок
                mov     [hErr],EAX
                invoke  GetCommandLineA         ;Запросим указатель на командную строку запуска
                mov     [CmdLine],EAX
                stdcall SkipFileName, CmdLine   ;Продвинем указатель за имя программы, на параметры
                stdcall InitPrimes              ;Решето Эратосфена до 2^16
                stdcall GetNumber, CmdLine      ;Возмём первый параметр как число
                test    EAX,EAX
                jz      .cycle                  ;Если отсутствует или неправильный или равен 0, то пропустим
                mov     EDX,EAX
                mov     ECX,0                   ;Количество простых по указанное число
                mov     EBX,0                   ;Текущее проверяемое число
.count:         stdcall isprime, EBX            ;Проверим на простоту
                add     ECX,EAX
                inc     EBX
                cmp     EBX,EDX
                jle     .count
                invoke  wsprintf, buf, .fmt3, ECX, EDX
                invoke  WriteFile, dword [hOut], buf, EAX, temp, 0
.cycle:         stdcall GetNumber, CmdLine      ;Считаем очередной параметр как число
                test    EAX,EAX
                jz      .exit                   ;Если отсутствует или неправильный или равен 0, то заканчиваем
                mov     [num],EAX               ;Сохраним на будущее
                stdcall isprime, EAX            ;Проверка на простоту
                mov     EDX,.fmt_no
                cmp     EAX,1                   ;Если простое, то будет =1
                jne     .no_prime
                mov     EDX,.fmt_prime
.no_prime:      invoke  wsprintf, buf, EDX, dword [num]
                invoke  WriteFile, dword [hOut], buf, EAX, temp, 0
                jmp     .cycle

.exit:          invoke  GetTickCount            ;Запросим текущее время в мс
                add     EAX,1                   ;Нулевых интервалов времени работы недопускаем, лучше пусть будет погрешность
                sub     EAX,[time0]             ;Вычтем время старта, получим время работы в мс
                xor     EDX,EDX                 ;Старшее слово обнулим
                mov     ECX,1000                ;Будем делить EDX,EAX на 1000 (количество мс в секунде)
                div     ECX                     ;Поделим время на два числа, в секундах и мс
                invoke  wsprintf, buf, .fmt1, EAX, EDX
                invoke  WriteFile, dword [hOut], buf, EAX, temp, 0
                invoke  ExitProcess,0
.fmt1:          db      'time: %u.%03us',13,10,0
.fmt_no:        db      '%u',13,10,0
.fmt_prime:     db      '%u is prime',13,10,0
.fmt3:          db      '%u primes up to %u',13,10,0

include 'cmdline_32.inc'
include 'isprime3_32.inc'

.data
num:            rd      1
time0:          rd      1
hOut:           rd      1
hErr:           rd      1
CmdLine:        rd      1
temp:           rd      1
buf:            rb      1000

.end main
Потом служебный cmdline_32.inc для получения чисел из командной строки с содержимым:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
proc SkipFileName stdcall uses EAX ECX, pStr:dword
                mov     ECX,[pStr]
                mov     ECX,[ECX]
.skip:          movzx   EAX,byte [ECX]
                test    EAX,EAX
                jz      .ret
                cmp     EAX,' '
                je      .ret
                inc     ECX
                cmp     EAX,'"'
                jne     .skip
.quotes:        movzx   EAX,byte [ECX]
                test    EAX,EAX
                jz      .ret
                inc     ECX
                cmp     EAX,'"'
                jne     .quotes
                jmp     .skip
.ret:           mov     EAX,[pStr]
                mov     [EAX],ECX
                ret
endp

proc GetNumber stdcall uses ECX EDX, pStr
                mov     ECX,[pStr]
                mov     ECX,[ECX]
                xor     EAX,EAX
.space:         movzx   EDX,byte [ECX]
                test    EDX,EDX
                jz      .ret
                inc     ECX
                cmp     EDX,' '
                je      .space
                dec     ECX
.cycle:         movzx   EDX,byte [ECX]
                test    EDX,EDX
                jz      .ret
                sub     EDX,'0'
                jc      .ret
                cmp     EDX,10
                jnc     .ret
                cmp     EAX,429496730   ;2^32/10+1
                jnc     .ovf
                lea     EAX,[EAX+EAX*4] ;EAX=EAX*5
                shl     EAX,1           ;EAX=EAX*10
                add     EAX,EDX
                jc      .ovf
                inc     ECX
                jmp     .cycle
.ovf:           invoke  wsprintf, buf, .fmt1
                invoke  WriteFile, dword [hErr], buf, EAX, temp, 0
                invoke  ExitProcess,1
.ret:           mov     EDX,[pStr]
                mov     [EDX],ECX
                ret
endp
.fmt1:          db      'Error: overflow in input number!',13,10,0
И наконец самый интересный isprime3_32.inc с содержимым:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
proc isprime stdcall uses EBX ECX EDX ESI, num
                mov     ECX,[num]               ;Проверяемое число
                mov     EAX,0                   ;Флаг что число не простое
                cmp     ECX,2
                jc      .ret                    ;Числа меньше 2 все не простые
                cmp     ECX,4
                jc      .yes                    ;Числа 2 и 3 простые
                test    ECX,1
                jz      .ret                    ;Остальные чётные числа все не простые
                cmp     ECX,max_prime
                jc      .low                    ;Число меньше размера таблицы простых, возьмём бит простоты напрямую
                mov     ESI,0                   ;Начнём проверять нечётные простые делители прямо с 1
                jmp     .prime
.check:         mov     EDX,0
                mov     EAX,ECX                 ;Проверяемое число
                lea     EBX,[ESI*2+1]           ;Делитель соответствующий биту ESI в таблице
                div     EBX
                mov     EAX,0
                test    EDX,EDX
                jz      .ret                    ;Разделилось без остатка, значит точно не простое
.next:          inc     ESI
                cmp     ESI,max_prime/2
                jnc     .yes                    ;Ни на что не разделилось, значит простое
.prime:         bt      [primes],ESI            ;Проверим простое ли число в ESI
                jnc     .next                   ;Если не простое, то пропустим деление
                jmp     .check                  ;Если простое, то попробуем на него поделить

.low:           shr     ECX,1                   ;Поделим число пополам (чётные не храним в таблице)
                bt      [primes],ECX            ;Возьмём бит из таблицы по смещению ECX в бит переноса
                jnc     .ret                    ;Если бит нулевой, то в EAX сохранился 0 и можно просто выйти
.yes:           mov     EAX,1                   ;Флаг что число простое
.ret:           ret
endp


proc InitPrimes stdcall uses EAX EBX
                mov     EAX,primes
                mov     dword [EAX],0xFFFFFFFE  ;Число 1 точно не простое, остальные нечётные под вопросом
.fill:          add     EAX,4                   ;Продвинем на 4 байта
                mov     dword [EAX],-1          ;Все единицы, все числа могут оказаться простыми
                cmp     EAX,primes+max_prime/16 ;Сравним на достижение конца таблицы (/16 потому каждый байт за 16 чисел отвечает)
                jc      .fill                   ;Пока меньше, заполняем
                mov     EBX,0                   ;Начнём просеивать с числа 3
.sieve:         inc     EBX
                cmp     EBX,sqrt_primes/2       ;Вычёркивать надо только нечётные числа меньшие 256
                jnc     .ret
                bt      [primes],EBX            ;Проверим очередное число (бит из таблицы в перенос)
                jnc     .sieve                  ;Если составное то продолжим поиск
                mov     EAX,EBX                 ;Нашли очередное простое, будем его вычёркивать
.clear:         lea     EAX,[EAX+EBX*2+1]       ;Продвигаем на следующее нечётное кратное найденному простому
                cmp     EAX,max_prime/2
                jnc     .sieve                  ;По достижению конца таблицы продолжим просеивание
                btr     [primes],EAX            ;Сбросим бит в таблице
                jmp     .clear
.ret:           ret
endp

max_prime = 65536                               ;Храним только нечётные до 65536, по 8 нечётных в байте
sqrt_primes = 256                               ;Корень из предела хранимых простых
.data
align 4
primes:         rb      max_prime/16
Контроля ошибок в них нет, и так немаленькие.

Если их все сложить в одну папку и скомпилировать, то запускать программу можно например так:
Код:
>prog3.exe 1000000 4294967279 4294967289 4294967291
78498 primes up to 1000000
4294967279 is prime
4294967289
4294967291 is prime
time: 10.578s
Количество чисел в параметрах можно указывать любое разумное, сначала она считает сколько простых до (включая) указанного первого числа, потом перечисляет остальные и показывает какие из них простые. Если первая функция не нужна, то указать первым числом 0.
Как видно скорость тотальной проверки на простоту (включая и все чётные) весьма невелика, порядка сотни тысяч в секунду. При том что числа до 65536, т.е. по имеющейся таблице, вычисляются за 0.001с, мгновенно, менее 1мс. Но до 70000 уже за 60мс.
Ещё, процедура IntPrimes это фактически решето Эратосфена для чисел до 65536 с битовым массивом только для нечётных чисел. Он и потом используется, так втрое меньше памяти надо чем если хранить все 6541 простое в прямом виде даже по 16 бит. Процедуру пришлось писать так как готовая таблица простых в прямом виде занимала больше 60К текста и на форум не влазит. Влезет ли битовая карта не проверял, через вычисление "на лету" красивее. Обе процедуры в файле isprime3_32.inc и основной код писались и отлаживались часа три, cmdline_32.inc взял практически готовым.

Если поменять две константы в конце isprime3_32.inc на 65536*16384 и 256*128 чтобы решето считало до $2^{30}$ и занимало 65МБ памяти, то оно будет шуршать 5.5с (primesieve успевает за 0.1с).

Пояснения будут следующим постом, а то уже необозримо. А потом займёмся ускорением isprime интересными методами.

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


20/08/14
11867
Россия, Москва
Сначала по новым командам.
Инструкция align компилятору выровнять текущий адрес на указанную границу (делимость). Чтобы следующие метки оказались по кратному 4 адресу. По идее не обязательно (хотя есть и исключения когда необходимо), но ускоряет доступ всего что длиннее байта. Лучше всегда выравнивать на величину размера структуры (здесь 32 бита или 4 байта). Про важное исключение расскажу когда про кэши речь будет.
Ну условные переходы jz/knz/je/jne/jle/jc/jnc пояснять нет нужды. Единственное, jc после cmp a,b позволяет узнать меньше ли a чем b.
Команда jmp переход и есть, всегда без всяких условий.
Команда test "логического И" без сохранения результата удобна для проверки регистра на ноль. Не портит перенос. Также удобно проверять отдельные биты, например младший для проверки (не)чётности или старший для выяснения знака (хотя и то и другое можно сделать и множеством других способов).
Команда div. Очевидно что-то куда-то делит. Если каждый регистр обозначить одной буквой, то она вычисляет X и Y из выражения AB/C=X+Y*C по известным A,B,C. Т.е. частное и остаток. Причём делимое может занимать два регистра, т.е. 64 бита (и 128 битов для x64), главное чтобы частное влезло в один регистр (т.е. A должно быть меньше C). Делимое всегда размещается в паре EDX,EAX (старшая часть в EDX), делитель может быть как регистром, так и ячейкой памяти (константой быть не может, беда), частное будет в EAX, остаток (всегда меньше делителя) в EDX. Т.е. делимое будет испорчено. Не слишком удобная команда. А уж какая медленная ... Делить на 0 не надо, как и нарушать условие A<C. Да, операция над беззнаковыми числами. Подставлять отрицательные числа ... получите бред или исключение (вылет программы по ошибке).
На самом деле вышесказанное только для деления на 32-разрядный операнд (и 64-разрядный, тогда регистры с буквой R вместо E), если делить 16-разрядные числа, то операция будет DX,AX/R=DX+AX*R. А если 8-разрядные, то AX/R=AH+AL*R. Но этими вариантами не пользуюсь, так что и не помню.
Movzx уже описывали, пересылка с дополнением нулями слева (старших битов). Посмотрите как в SkipFileName пользуюсь ей чтобы не париться с байтовыми регистрами, а работать только с 32-разрядными.
Команда shr r,1 (и shr r,cl) это сдвиг числа вправо (к младшим битам) на один бит (или на столько бит, какое число в регистре CL). Эффективно делит беззнаковое число на степень двойки с округлением вниз. Эквивалент операции a\2 в PARI/GP или floor(a/2).
Команда bt извлекает бит из ячейки памяти (или регистра) в флаг переноса по его номеру в регистре (или константе), причём не обязательно только 32 или 64 бита, можно хоть квинтиллион битов, лишь бы в память влезли. Очень удобно что не надо делить номер бита на части <адрес_ячейки_памяти>:<номер_в_ней>.
Команда btr кроме чтения бита в перенос после этого его ещё и сбрасывает. Для установки бита есть команда bts. И тоже принимает большой номер бита если массив битов не в регистре. Удобно.
Ну и команда lea. Складывает от одного до до трёх чисел, два из которых в регистрах, один из которых можно домножить на 2,4,8, и константу (можно и не указывать). Изначально задумывалась для вычисления адресов ячеек памяти (потому и правый операнд в []), но оказалась удобной и в других случаях, экономит до 3-х команд. Позволяет вычислять величины x, 2x, 3x, 4x, 5x, 8x, 9x, плюс по желанию и константа, по любому регистру x. Но несколько медленнее эквивалентной цепочки команд, потому использовать с аккуратностью.

Вообще по командам есть неплохой сайт на русском: http://www.club155.ru/x86cmdcpu (кажется про него уже упоминал). Он похож на перевод мануала Intel, без отсебятины. Это и хорошо и плохо: хорошо что без лишних фантазий, плохо что бывает невнятно или слишком сухо, без подробных пояснений (ровно как и у самой Intel). Правда инфа только по x32 и без AVX.
Там же рядом есть и описание регистров и форматов данных и прочего (ненужного).

Команду ret и вообще работу с функциями опишу дальше, там тонкостей много.

А пока, бегло по алгоритму.
В основной программе получаем от винды текущее время чтобы потом заценить скорость, пару (не)нужных хэндлов (чтобы вывод текста попал в окно консоли), указатель на командную строку. И начинаем разбирать последнюю, пропуская имя запущенной программы (с полным путём, бывает удобно). Но сначала вызовем решето Эратосфена чтобы оно нам сгенерировало и запомнило все нечётные простые до 65536, которых достаточно для проверки на простоту чисел до $2^{32}$. А потом берём первый аргумент из строки запуска и считаем сколько до него простых, перебирая числа вообще подряд (пример же) и проверяя их простоту. Результат выводим.
Дальше цикл пока запрос очередного числа не вернёт 0 в качестве флага или ошибки, или конца списка чисел. И каждое из них снова проверяем на простоту и выводим один из двух разных форматов текста. Собственно всё.
Коротко по функции SkipFileName. Получает указатель на указатель на строку (т.е. указатель на двойное слово в памяти где лежит указатель на строку) и продвигает его до первого пробела вне двойных кавычек. Ничего не возвращает, указатель продвинулся там где и был (плохая практика!). Возможные ошибки - на совести юзера. С русскими символами и пробелами в пути работает правильно.
Функция GetNumber тоже получает указатель на ячейку с указателем на строку параметров, пытается превратить в число и пока удаётся для каждой цифры домножает накопленное значение на 10 и добавляет текущую цифру. Как цифры кончились - всё, число получено, сохраняет указать на остаток строки где он и был и возвращает накопленное число в EAX. Если числа нет или оно не из цифр, возвращает 0 как признак ошибки (неудобно, но меня устраивало). Все ошибки в десятичной записи числа - опять на совести юзера. Единственно что делает - проверяет чтобы число влезало в регистр, если строка представляет число больше, то выводит ошибку и сама завершает всю программу, не возвращая управление вызвавшему. Ну вот так. Попробуйте запустить с длинным числом, порадуетесь.
Функция InitPrimes без параметров реализует решето Эратосфена для чисел до 65536 (ну или сколько укажете, только помните про память). Сначала заполняет весь массив (без младшего бита, это число 1, оно не простое) единицами, потом перебирает индексы в этом массиве битов пока не найдёт единичный бит, значит этому индексу соответствует очередное простое число и надо его вычеркнуть дальше до конца массива. И вычёркивается. Как добрались, переходим к поиску перебором следующего простого числа. А если вычеркнули все (до $\sqrt{65536}=256$), то завершаем функцию. Ничего не возвращаем. И ошибки не проверяем (типа указали кривые константы, max_prime должна быть кратна 64, а вторая равна корню из неё).
Функция isprime основная. Сначала проверяем малые простые, потом на чётность. Если не ушли, то проверяем что число мало и "его бит" есть в таблице, тогда просто берём его и возвращаем в регистре результата EAX. Если число больше, то придётся проверять на делимость. Начинаем с первого же бита в массиве (это число 1), если он 1 (т.е. текущий делитель простой), то делим и проверяем остаток, нулевой - беда, число составное, уходим на выход. Если ненулевой, то ищем перебором подряд следующий простой делитель и повторяем до конца таблицы (блин, забыл сделать проверку на квадрат чтобы заканчивать раньше, придётся сделать как оптимизацию). Делается сколько-то лишних действий (перебор нулевых битов в массиве), но по сравнению со скоростью деления это мелочи.
Как-то так.

-- 19.01.2024, 00:35 --

Коротко расскажу как писал эти две функции, в isprime3_32.inc.
Сначала поставил вызов InitPrimes в main и пошёл в файлик isprime3_32.inc. Написал цикл заполнения, это просто. После него сделал цикл поиска перебором единичного бита (понятно они пока все такие), от метки .sieve до jnc .sieve. Теперь осталось найденное простое аккуратно вычеркнуть. Чтобы не попортить EBX копирую его в EAX и думаю как прибавлять простое из EBX, ведь там не само число, а его индекс в битовом массиве. Пара минут размышлений и формула +2x+1 в кармане. Проверка "в уме" 3-4 первых значений показывает правильность. Вспоминаю про команду lea, как раз повод её применить. Ок, добавил, сбрасываю бит командой btr, зацикливаю и думаю когда же остановиться ... Да по концу таблицы очевидно. И уйти в поиск нового простого. Всё.
В isprime сразу написал начало до jc .low и конец с метки .low, это просто. Потом отступив написал деление и инициализацию регистров для него. Добавил проверку остатка и правильный выход если разделилось. Если не, то увеличение делителя с проверкой на конец таблицы (если дошли, значит простое). И пропуск составных делителей. Закольцевал, состыковал правильно, и вуаля.
Потом вернулся в main и написал цикл перебора параметров, но как-то всё моментально отрабатывалось, пришлось добавить тупейший первый цикл подсчёта количества.
Разумеется были тестовые запуски, сравнение с априори известными величинами, даже отладчик пару раз запускал разобраться как параметры передаются.
Это я к тому что оно не сразу такое прям прекрасное родилось, раз 10 переставлял куски кода местами чтобы покрасивше смотрелись условные переходы и вообще логика сохранялась. И писал постепенно, почти законченными кусочками (хоть их и не проверить было до самого конца).

PS. Всё собираюсь с духом про вызов функций писать ...

-- 19.01.2024, 00:38 --

Можно наверное пока вопросы, кроме как именно вызываются процедуры и что по этому поводу думает (и видит в коде) проц, про них думаю уже завтра (ближе к вечеру) будет.

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


20/08/14
11867
Россия, Москва
Dmitriy40 в сообщении #1626448 писал(а):
(блин, забыл сделать проверку на квадрат чтобы заканчивать раньше, придётся сделать как оптимизацию)
Не, это неплохое задание на дом: добавить в isprime проверку что текущий делитель стал больше корня из тестируемого числа и не лопатить делители до конца таблицы.
Корень вычислять не будем, изврат это, использовать 32-разрядную команду MUL. Проверку добавить перед делением.
Протестировать результат на известных простых и составных, особенно на квадратах простых, обязательно на $65521^2, 65521\cdot65537$. Оценить выигрыш времени в сравнении с исходным вариантом.
Кстати интересный результат будет по времени. Думаю оптимизация ухудшит скорость! ;-) Если да (а я не проверял), то подумать отчего так.
Если окажется слишком легко, то подумать можно ли отказаться от команды умножения и ограничиться командами сложения/вычитания/сравнения. Дописать и проверить.
Если для кого и это очевидно, не подсказывайте. ;-)

Да, про время. Хоть оно запрашивается и выводится с точностью в 1мс, но реально оно может плавать вплоть до десятых долей секунды. Потому для более-менее точного анализа надо брать общее время работы не менее десятка секунд (единицы секунд ещё слишком шумно), лучше даже до минуты (и тогда отбрасывать доли секунд). Только так случайные выбросы уйдут ниже погрешностей. А ещё желательно повторить идентичные запуски 3-5 раз и взять медиану (приблизительно среднее).
Да, я считаю именно такой способ наиболее адекватным для оценки реального времени работы программ. А все счётчики и выбор наименьшего времени - лишь для теоретической оценки.

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


29/04/13
8307
Богородский
Dmitriy40, Спасибо Огромное, Ваши проги у меня работают и контрольную сумму, то бишь $\pi(x)$ считают правильно и на простоту проверяют тоже правильно.

Ещё не прочитал все комменты. ДЗ буду выполнять, прошу не торопить и не подсказывать.

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

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



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

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


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

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