Подтвердить что ты не робот

Более высокая часть умножения и деления на C или С++?

Когда я умножаю пару целых чисел 4 байта в сборке, нижняя часть результата находится в EAX и более высокая часть в EDX. Если я нахожусь на C или С++ и хочу получить более высокую часть, возможно ли это без использования встроенной сборки?

Точно так же можно получить результат целочисленного деления от EAX, а модуль - результат EDX без повторения деления на C или С++? Я действительно знаю только сначала a/b, а затем a%b, тогда как в ассемблере оба результата даются в одной и той же операции.

4b9b3361

Ответ 1

Вы можете сделать это легко с помощью C таким образом:

#include <stdint.h>

uint32_t a, b;  // input
uint64_t val = (uint64_t)a * b;
uint32_t high = val >> 32, low = val;

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

Как прокомментировал Пит Беккер, вышеупомянутое полагается на доступность типов uint32_t и uint64_t. Если вы настаиваете на жесткой переносимости (например, вы программируете на DS9K), вместо этого вы можете использовать типы uint_least32_t и uint_least64_t или uint_fast32_t и uint_fast64_t, которые всегда доступны под C99, но вам нужно дополнительная маска, которая будет оптимизирована, если не требуется:

#include <stdint.h>

uint_fast32_t a, b;  // input
uint_fast64_t val = (uint_fast64_t)a * b;
uint_fast32_t high = (val >> 32) & 0xFFFFFFFF, low = val & 0xFFFFFFFF;

Что касается деления, вы можете использовать функции библиотеки C99 div, ldiv или lldiv для выполнения подписанных операций деления и останова в одном вызове. Комбинация разделение/модуляция будет реализована в одной операции, если это возможно, в целевой архитектуре для конкретных типов операндов.

Возможно, более эффективно писать оба выражения и полагаться на компилятор для обнаружения шаблона и создания кода, который использует один код операции IDIV:

struct divmod_t { int quo, rem; };
struct divmod_t divmod(int num, int denom) {
    struct divmod_t r = { num / denom, num % denom };
    return r;
}

Тестирование в Проводник компилятора Matt Godbolt показывает, что clang и gcc генерируют одну команду idiv для этого кода в -O3.

Вы можете превратить одно из этих делений в умножение:

struct divmod_t { int quo, rem; };
struct divmod_t divmod2(int num, int denom) {
    struct divmod_t r;
    r.quo = num / denom;
    r.rem = num - r.quo * denom;
    return r;
}

Обратите внимание, что указанные выше функции не проверяют потенциальное переполнение, что приводит к поведению undefined. Переполнение происходит, если denom = 0 и если num = INT_MIN и denom = -1.

Ответ 2

Вы не имеете дело с деталями реализации на C или С++. Это все. Если вам нужны самые важные байты, просто используйте язык. Для этого предназначен правый сдвиг >>. Что-то вроде:

uint64_t i;
uint32_t a;
uint32_t b;
// input a, b and set i to a * b
// this should be done with (thanks to @nnn, pls see comment below):
// i = a; i *= b;
uint64_t msb = i >> 32;

Ответ 3

Для умножения только Forth среди широко известных языков (выше ассемблера) имеет явное умножение N * N бит на 2N-бит результат (слова M*, UM*). C, Fortran и т.д. Не имеют этого. Да, это иногда приводит к неправильной оптимизации. Например, на x86_32 получение 64-разрядного продукта требует либо преобразования числа в 64-разрядный (может вызвать вызов библиотеки вместо команды mul), либо явного встроенного сборочного вызова (простого и эффективного в gcc и клоне, но не всегда в MSVC и других компиляторах).

В моих тестах на x86_32 (i386) современный компилятор способен конвертировать код типа

#include <stdint.h>
int64_t mm(int32_t x, int32_t y) {
  return (int64_t) x * y;
}

для простой инструкции "imull" без вызова библиотеки; clang 3.4 (-O1 или выше) и gcc 4.8 (-O2 или выше) удовлетворяет этому, и я думаю, это не остановится никогда. (При меньшем уровне оптимизации добавляется второе ненужное умножение). Но нельзя гарантировать это для любого другого компилятора без реального теста. С gcc на x86 следующие работы будут работать даже без оптимизации:

int64_t mm(int32_t x, int32_t y) {
  int64_t r;
  asm("imull %[s]" : "=A" (r): "a" (x), [s] "bcdSD" (y): "cc");
  return r;
}

Такая же тенденция, с аналогичными командами, верна для почти всех современных процессоров.

Для деления (например, 64-битного дивиденда на 32-разрядный делитель на 32-битный коэффициент и остатки) это сложнее. Существуют библиотечные функции, такие как `lldiv ', но они предназначены только для подписанного деления; нет беззнаковых эквивалентов. Кроме того, они являются библиотечными вызовами со всеми соответствующими расходами. Но проблема в том, что многие современные архитектуры не имеют такого разделения. Например, он явно исключается из ARM64 и RISC-V. Для них нужно эмулировать длинное деление с использованием более короткого (например, деление 2 ** (N-1) на дивиденд, но затем удвоить результат и настроить его остаток). Для тех, у кого есть разделение по смешанной длине (x86, M68k, S/390 и т.д.), Однострочный сборник inliner довольно хорош, если вы уверены, что он не будет переполняться:)

В некоторых архитектурах отсутствует поддержка разделов (более старая Sparc, Alpha) и стандартная задача библиотеки для поддержки таких операций.

В любом случае стандартная библиотека предоставляет все необходимые операции, если только вам не нужна максимальная точность (например, x86_64 может делить 128-битный дивиденд на 64-разрядный делитель, но это не поддерживается библиотекой C).

Я думаю, что наиболее разработанный и доступный пример этих подходов для разных архитектур библиотека GMP. Это намного более продвинутый, чем ваш вопрос, но вы можете выкапывать примеры для деления на одну конечность для разных архитектур, он реализует правильную цепочку, даже если архитектура не поддерживает ее напрямую. Кроме того, этого будет достаточно всего для произвольной длинной арифметики чисел, несмотря на некоторые накладные расходы.

NB, если вы вызываете div -подобную инструкцию явно, это ваша ответственность за проверку переполнения. Это более сложное в подписанном случае, чем в неподписанном; например, разделение -2147483648 на -1 приводит к сбою программы на основе x86, даже если она написана на C.

Ответ 4

Для деления полностью портативное решение использует одну из библиотечных функций div, ldiv или lldiv.