Сначала по новым командам.
Инструкция 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, которых достаточно для проверки на простоту чисел до
. А потом берём первый аргумент из строки запуска и считаем сколько до него простых, перебирая числа вообще подряд (пример же) и проверяя их простоту. Результат выводим.
Дальше цикл пока запрос очередного числа не вернёт 0 в качестве флага или ошибки, или конца списка чисел. И каждое из них снова проверяем на простоту и выводим один из двух разных форматов текста. Собственно всё.
Коротко по функции SkipFileName. Получает указатель на указатель на строку (т.е. указатель на двойное слово в памяти где лежит указатель на строку) и продвигает его до первого пробела вне двойных кавычек. Ничего не возвращает, указатель продвинулся там где и был (плохая практика!). Возможные ошибки - на совести юзера. С русскими символами и пробелами в пути работает правильно.
Функция GetNumber тоже получает указатель на ячейку с указателем на строку параметров, пытается превратить в число и пока удаётся для каждой цифры домножает накопленное значение на 10 и добавляет текущую цифру. Как цифры кончились - всё, число получено, сохраняет указать на остаток строки где он и был и возвращает накопленное число в EAX. Если числа нет или оно не из цифр, возвращает 0 как признак ошибки (неудобно, но меня устраивало). Все ошибки в десятичной записи числа - опять на совести юзера. Единственно что делает - проверяет чтобы число влезало в регистр, если строка представляет число больше, то выводит ошибку и сама завершает всю программу, не возвращая управление вызвавшему. Ну вот так. Попробуйте запустить с длинным числом, порадуетесь.
Функция InitPrimes без параметров реализует решето Эратосфена для чисел до 65536 (ну или сколько укажете, только помните про память). Сначала заполняет весь массив (без младшего бита, это число 1, оно не простое) единицами, потом перебирает индексы в этом массиве битов пока не найдёт единичный бит, значит этому индексу соответствует очередное простое число и надо его вычеркнуть дальше до конца массива. И вычёркивается. Как добрались, переходим к поиску перебором следующего простого числа. А если вычеркнули все (до
), то завершаем функцию. Ничего не возвращаем. И ошибки не проверяем (типа указали кривые константы, 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 --Можно наверное пока вопросы, кроме как именно вызываются процедуры и что по этому поводу думает (и видит в коде) проц, про них думаю уже завтра (ближе к вечеру) будет.