Работа с 64-битными произведениями и частными 32-битных целых чисел в сборке x86-64

Начав изучать ассемблер x86-64, я пишу программу, которая получает массив целых чисел и выполняет над ним некоторые вычисления. Цель не имеет отношения к вопросу, но вычисления включают умножение и деление, размер и знак целых чисел неизвестны, и программа должна обрабатывать любой случай.
Интересно, как правильно это сделать? Должен ли я расширить целые числа до размера регистра x64 или мне следует работать с расширениями EDX:EAX?

Моя первая попытка:

    movl (%r8), %edi #r8 holds the memory address of the array
    movl 4(%r8), %r10d # assume those are valid elements in the array, the check is done above 
    movl 8(%r8), %eax 
    imul %edi, %eax
    imul %r10d, %r10d
    cdq
    idiv %r10d 

но, насколько я понимаю, с помощью этих инструкций, если умножение вызывает переполнение, eax становится 0 и включается флаг переноса, поэтому деление на r10d может вызвать SIGFPE из-за деления на 0.
Какой лучший подход к этому? Я склонен работать с 64-битными регистрами, поэтому мне будет легче справиться со всеми случаями, но я могу ошибаться.

Спасибо, я изменил размер регистров на 64 бита, и в большинстве случаев работает, единственная проблема заключается в том, что умножение -1 на -1 или деление на -1 все равно вызывает sigfpe, что мне с этим делать?

Newlearner826 04.07.2024 09:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
84
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

x86-64 — это 64-битная архитектура, поэтому эффективный код bigint обычно должен работать 64-битными фрагментами. например imul %rdi, чтобы установить 128-битный RDX:RAX для произведения RDI * RAX. Используйте для этого форму с одним операндом imul (https://www.felixcloutier.com/x86/imul). Формы с большим количеством операндов, которые вы используете, не записывают неявно EDX или RDX.

Если 64 бита достаточно широки, вы можете использовать 64-битную математику без расширения после знакового расширения входных данных с помощью таких нагрузок, как movslq (%r8), %rax. Затем 2-операнд imul для умножения, не нарушая RDX. Вам понадобится cqo / idiv, если вы хотите выполнить 64-битное деление. Синтаксис AT&T называет это cqto, но GAS принимает любую мнемонику.

(Если значения в памяти могут быть просто 64-битными, вы можете использовать такие вещи, как imulq 8(%r8), %rax вместо использования mov для загрузки.)

Если вы знаете, что частное умещается в 32 бита, mov 8(%r8), %eax/imull (%r8) получит ваш 64-битный продукт в EDX:EAX (обратите внимание на явный суффикс размера операнда l в imul), который устанавливает 32-битный размер операнда. idivl 4(%r8). В этом случае вы не используете cdq, это заменит ваш 64-битный продукт расширением знака младшей половины. (Когда и почему мы подписываем расширение и используем cdq с mul/div? / Почему EDX должен быть равен 0 перед использованием инструкции DIV?).

Но использование полной 64-битной ширины делимого для 32-битного деления размера операнда приводит к тому, что частное не помещается в размер операнда (для случаев, отличных от INT_MIN / -1 или деления на 0) , и в этом случае вы получите исключение #DE. (И операционные системы POSIX, включая Linux, доставят SIGFPE в ваш процесс.)

Кроме того, это ограничивает ваш делитель до 32-битного. Вы говорите, что возведение в квадрат r10d может переполниться, и это еще одна причина, по которой это не вариант.


64-битный размер операнда для деления работает намного медленнее на процессорах Intel до Ice Lake, но если ваш делитель не помещается в 32 бита, это ваш единственный вариант. (За исключением ветвления по размеру делителя, что может быть преимуществом для старых процессоров Intel; некоторые версии clang/опции оптимизации делают это.)

С 32-битными входными данными со знаком и использованием 64-битных целых чисел для временных значений. Хранение 64-битных целых чисел в отдельных регистрах для простоты и эффективности. (два операнда imul — это один моп, а один операнд — это 2 или 3 мопа на Intel, поскольку он должен записать 2 выходных регистра и разделить выход блока умножителя. https://uops.info/)

    movslq (%r8), %rdi   # sign-extend long (32-bit) to quad (64-bit)
    movslq 4(%r8), %r10
    movslq 8(%r8), %rax 

    imul   %rdi, %rax    # 64-bit non-widening multiplies can't overflow since the values are 32-bit
    imul   %r10, %r10

    cqto                # signed division of RAX by R10
    idiv   %r10

Даже квадрат INT_MIN (-2^31) по-прежнему помещается в int64_t, поэтому эти умножения не могут привести к переполнению. Не было необходимости в imul %rdi, который записывал бы полный 128-битный продукт в RDX:RAX. Но это позволит избежать необходимости в cqto в данном случае, поскольку для целочисленного деления x86 действительно требуется делимое двойной ширины.

На самом деле это дало бы нам меньший машинный код и обеспечило бы безубыточность по количеству операций на последних процессорах Intel и AMD (https://uops.info/), где imul r64 — это «всего» 2 мопса, а задержка записи RDX равна всего через один цикл после того, как результат RAX готов (так же, как cqo).

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

Это работает идеально, за исключением случаев, когда я пытаюсь возвести в квадрат -1, а затем разделить на результат квадрата, почему это происходит?

Newlearner826 04.07.2024 09:41

@Newlearner826: Каковы исходные три 32-битных входа? Какие значения находятся в RAX и R10 перед idiv? (Одношаговый с отладчиком). Не должно возникнуть проблем с делением на 1 с использованием cqto/idiv, поскольку делимое уже представляет собой расширенное знаком 64-битное целое число (из cqto), поэтому оно не может переполниться ни для одного делимого.

Peter Cordes 04.07.2024 09:44

Другие вопросы по теме