Получение старшей и младшей половин полного целочисленного умножения

Я начинаю с трех значений A, B, C (32-битное целое число без знака). И мне нужно получить два значения D, E (также 32-битное целое число без знака). Где

D = high(A*C);
E = low(A*C) + high(B*C);

Я ожидаю, что умножение двух 32-битных uint даст 64-битный результат. «Высокий» и «Низкий» - это просто мои обозначения для обозначения первых 32 бита и последних 32 бита в 64-битном результате умножения.

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

Есть несколько инструкций SSE, которые подходят для вычисления упомянутой процедуры. Компилятор gcc, вероятно, оптимизировал работу. Однако я не отвергаю возможность написать какой-то фрагмент кода напрямую в SSE, если это будет необходимо.

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

  1. Сохраните четыре 32-битных целых числа в одном 128-битном регистре по порядку: A, B, C, C.
  2. Применить инструкцию (вероятно, pmuludq) в упомянутый 128-битный регистр, которая умножает пары 32-битных целых чисел и в результате возвращает пары 64-битных целых чисел. Таким образом, он должен одновременно вычислять умножение A*C и умножение B*C и возвращать два 64-битных значения.
  3. Я ожидаю, что у меня будут новые 128-битные значения регистров P, Q, R, S (четыре 32-битных блока), где P, Q - это 64-битный результат A*C и R, S - 64-битный результат B*C. Затем я продолжаю переставлять значения в регистре в порядке P, Q, 0, R
  4. Возьмите первые 64 бита P, Q и добавьте вторые 64 бита 0, R. Результат - новое 64-битное значение.
  5. Считайте первые 32 бита результата как D и последние 32 бита результата как E.

Этот алгоритм должен возвращать правильные значения для E и D.

Мой вопрос:

Есть ли в С ++ статический код, который генерирует подобную процедуру SSE, как упоминалось в алгоритме 1-5 SSE? Я предпочитаю решения с более высокой производительностью. Если алгоритм проблематичен для стандартных команд c ++, есть ли способ написать алгоритм в SSE?

Я использую 64-битный компилятор TDM-GCC 4.9.2.

(примечание: вопрос был изменен после совета)

(примечание 2: меня вдохновляет этот http://sci.tuomastonteri.fi/programming/sse для использования SSE для повышения производительности)


person Marek Basovník    schedule 02.02.2016    source источник
comment
Мне непонятно, о чем вы спрашиваете. Вы хотите знать, может ли операция переполниться?   -  person NathanOliver    schedule 02.02.2016
comment
Обратите внимание, что 4294967296 равно 1ull << 32.   -  person Jarod42    schedule 02.02.2016
comment
Использование uint64_t кажется более простым и эффективным, чем альтернативы (например, умножение вручную с uint16_t).   -  person Jarod42    schedule 02.02.2016
comment
Натан Оливер: Не если, а сколько раз. Мне просто интересно, есть ли в С ++ умная команда для операции A * B / C без потери точности и с использованием типов с двойным размером памяти. Один из примеров - деление на 2 ^ 32 (где деление можно интерпретировать как количество переполнений).   -  person Marek Basovník    schedule 02.02.2016
comment
@ MarekBasovník Целочисленные переполнения не отображаются во время выполнения. Как реализованы переполнения, зависит от компилятора. Таким образом, нет никакого способа их посчитать.   -  person πάντα ῥεῖ    schedule 02.02.2016
comment
Как я уже сказал в комментариях к ответу Грабуса, x86 имеет умножение 32b * 32b - ›64b (которое уже дает высокую и низкую половину результата в отдельных регистрах, все настроено для выполнения low32(A*C) + high32(C*B). Итак, если вам просто нужны обе половины результат умножения 32b, используйте его. В любом случае, возможно, стоит векторизовать его, в зависимости от того, как выглядит окружающий код. Если вы намекнете об этом, возможно, мы сможем помочь вам векторизовать. Ваша версия этого документа до редактирования определенно была проблема XY   -  person Peter Cordes    schedule 03.02.2016
comment
Этот вопрос действительно нужно отредактировать, например, получить высокую половину полного целочисленного умножения или что-то в этом роде. Я думаю, что исходный вопрос (умножить, разделить на большую степень двойки, а затем подсчитать переполнение) - это чепуха, и ее нужно просто уйти. Но я думаю, что это слишком большая правка для кого-то другого, так что вам стоит это сделать. Для того, чтобы сформулировать вопрос, может потребоваться гораздо меньше слов, теперь, когда вы увидели, каков может быть ответ, и можете задать его лучше. :П   -  person Peter Cordes    schedule 03.02.2016
comment
Ты прав. Я попробую.   -  person Marek Basovník    schedule 03.02.2016
comment
Действительно, есть инструкции SSE, которые дают высокую половину или полный продукт, но они не так просты в использовании (и даже не обязательно быстрее), чем использование встроенного типа двойной ширины. Это, конечно, и проще, и быстрее, чем ручная очистка с помощью узких частичных продуктов и эмулированной надстройки с переносом.   -  person harold    schedule 04.02.2016


Ответы (2)


Для этого вам не нужны векторы, если у вас нет нескольких входов для параллельной обработки. clang и gcc уже хорошо справляются с оптимизацией «нормального» способа написания кода: приведение к двойному размеру, умножение, затем сдвиг, чтобы получить высокую половину. Компиляторы распознают этот шаблон.

Они замечают, что операнды начинались как 32-битные, поэтому все верхние половины равны нулю после преобразования в 64b. Таким образом, они могут использовать mul insn x86 для умножения 32b * 32b-> 64b вместо выполнения полного умножения на 64b с расширенной точностью. В 64-битном режиме они делают то же самое с __uint128_t версией вашего кода.

Обе эти функции компилируются в довольно хороший код (по одному mul или imul на умножение).. gcc -m32 не поддерживает типы 128b, но я не буду вдаваться в подробности, потому что 1. вы спрашивали только о полных умножениях 32-битных значений и 2. вы всегда должны использовать 64-битный код, когда хотите, чтобы что-то работало быстро. Если вы выполняете полное умножение, когда результат не помещается в регистр, clang позволит избежать множества дополнительных инструкций mov, потому что gcc глупо об этом. Эта небольшая тестовая функция послужила хорошим тестовым примером для отчета об ошибке gcc.

Эта ссылка на Godbolt включает функцию, которая вызывает это в цикле, сохраняя результат в массиве. Он автоматически векторизуется с кучей перетасовки, но по-прежнему выглядит как ускорение, если у вас есть несколько входов для параллельной обработки. Другой формат вывода может потребовать меньше перетасовки после умножения, например, сохранение отдельных массивов для D и E.

Я включаю версию 128b, чтобы показать, что компиляторы могут справиться с этим, даже если это нетривиально (например, просто выполните 64-битную инструкцию imul, чтобы выполнить умножение 64 * 64-> 64b на 32-битных входах, после обнуления любых старших битов, которые могут быть сидит во входных регистрах при вводе функции.)

При нацеливании на процессоры Haswell и новее, gcc и clang могут использовать инструкцию mulx BMI2. (Я использовал -mno-bmi2 -mno-avx2 в ссылке godbolt, чтобы упростить asm. Если у вас есть процессор Haswell, просто используйте -O3 -march=haswell.) mulx dest1, dest2, src1 выполняет dest1:dest2 = rdx * src1, а mul src1 rdx:rax = rax * src1. Итак, mulx имеет два входа только для чтения (один неявный: _17 _ / _ 18_) и два выхода только для записи. Это позволяет компиляторам выполнять полное умножение с меньшим количеством mov инструкций для ввода и вывода данных в неявные регистры для mul. Это лишь небольшое ускорение, особенно. поскольку 64-битный mulx имеет задержку 4 цикла вместо 3 на Haswell. (Как ни странно, 64-битные mul и mulx немного дешевле 32-битных mul и mulx.)

// compiles to good code: you can and should do this sort of thing:
#include <stdint.h>

struct DE { uint32_t D,E; };

struct DE f_structret(uint32_t A, uint32_t B, uint32_t C) {
  uint64_t AC = A * (uint64_t)C;
  uint64_t BC = B * (uint64_t)C;
  uint32_t D = AC >> 32;         // high half
  uint32_t E = AC + (BC >> 32);  // We could cast to uint32_t before adding, but don't need to
  struct DE retval = { D, E };
  return retval;
}



#ifdef __SIZEOF_INT128__  // IDK the "correct" way to detect __int128_t support
struct DE64 { uint64_t D,E; };

struct DE64 f64_structret(uint64_t A, uint64_t B, uint64_t C) {
  __uint128_t AC = A * (__uint128_t)C;
  __uint128_t BC = B * (__uint128_t)C;
  uint64_t D = AC >> 64;         // high half
  uint64_t E = AC + (BC >> 64);
  struct DE64 retval = { D, E };
  return retval;
}
#endif
person Peter Cordes    schedule 04.02.2016

Если я правильно понимаю, вы хотите вычислить количество потенциальных переполнений в A * B. Если да, то у вас есть 2 хороших варианта - «использовать вдвое большую переменную» (написать 128-битную математическую функцию для uint64 - это не так уж сложно (или подождите, пока я опубликую ее завтра)) и «использовать тип с плавающей запятой»: (float (A) * float (B)) / float (C), поскольку потеря точности минимальна (при условии, что float составляет 4 байта, двойной 8 байтов и длинный двойной 16 байтов), а для float и uint32 требуется 4 байта памяти (используйте double для uint64_t, так как он должен быть длиной 8 байт):

#include <iostream>
#include <conio.h>
#include <stdint.h>

using namespace std;

int main()
{
    uint32_t a(-1), b(-1);
    uint64_t result1;
    float result2;
    result1 = uint64_t(a)*uint64_t(b)/4294967296ull;    // >>32 would be faster and less memory consuming
    result2 = float(a)*float(b)/4294967296.0f;
    cout.precision(20);
    cout<<result1<<'\n'<<result2;
    getch();
    return 0;
}

Производит:

4294967294
4294967296

Но если вам нужен действительно точный и правильный ответ, я бы предложил использовать для вычислений вдвое больший шрифт.

Теперь, когда я думаю об этом - вы можете использовать long double для uint64 и double для uint32 вместо написания функции для uint64, но я не думаю, что это гарантирует, что long double будет 128 бит, и вам придется это проверить. Я бы выбрал более универсальный вариант.


РЕДАКТИРОВАТЬ:

You can write function to calculate that without using anything more
than A, B and result variable which would be of the same type as A.
Just add rightmost bit of (where Z equals B*(A>>pass_number&1)) Z<<0,
Z<<1, Z<<2 (...) Z<<X in first pass, Z<<-1, Z<<0, Z<<1 (...) Z<<(X-1)
for second (there should be X passes), while right shifting the result
by 1 (the just computed bit becomes irrelevant to us after it's
computed as it won't participate in calculation anymore, and it would
be erased anyway after dividing by 2^X (doing >>X)

(пришлось поместить в «код», так как я здесь новичок и не могу найти другого способа, чтобы скрипт форматирования не съел половину его)

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

Я напишу код для этого завтра, если вам все еще понадобится помощь.

person Grabusz    schedule 02.02.2016
comment
Спасибо за ваш ответ. Я вижу, что, вероятно, нужна дополнительная информация о проблеме. Итак, я отредактировал свой вопрос. - person Marek Basovník; 03.02.2016
comment
@MarekBasovnik под левой частью умножения C B вы имеете в виду что-то вроде C B mod 2 ^ 32? (напоминание о делении C B на 2 ^ 32), или сколько раз C B переполнял unsigned int? Новое объяснение меня сильно сбивает с толку. Я также не знаю, что вы имеете в виду, выбрасывая часть переполнения ... некоторые компиляторы аннулируют ваш int и отмечают его как непригодный для использования, если вы их переполняете. И я бы не стал добавлять только несколько строк сборки, так как это может замедлить работу программы (компилятор не может оптимизировать код так, как он хочет). - person Grabusz; 03.02.2016
comment
Я математик, а не программист. Вот почему моя формулировка немного расплывчата, извините. Мы знаем, что умножение 2 целых чисел с некоторым диапазоном N обычно дает результат как целое с диапазоном 2 × N. Если я посмотрю на набор инструкций SSE2, есть операция по умножению двух 32-битных целых чисел с полным неотрезанным 64-битным результатом, сохраненным в регистре (поэтому я ожидаю простого решения в коде C ++). Мне просто нужно получить результат умножения двух целых чисел и обеих частей (первые 32 бита и вторые 32 бита), которые мне нужно использовать для разных целей (хранить их в разных значениях). - person Marek Basovník; 03.02.2016
comment
@ MarekBasovník: инструкция mul x86 выполняет полное умножение 32b * 32b- ›64b, сохраняя результат в edx:eax. 64-битная версия делает 64b * 64b- ›128b (rdx:rax). Инструкция деления x86 делит rdx:rax по источнику, сохраняя частное в rax, а остаток в rdx. (Так что это операция 128b / 64b- ›64b или 64b / 32b-› 32b). Ищите оболочку для этих операций, которую вы можете использовать в C ++, вместо того, чтобы пытаться фактически обнаружить переполнение и что-то с этим сделать. Вам также может быть полезен gmplib.org. - person Peter Cordes; 03.02.2016
comment
@Marek: Если вы делаете что-то, что может выиграть от параллельного выполнения нескольких умножений, то обязательно взгляните на SSE (особенно SSE4.1 принес много хороших вещей для 32/64-битных целых чисел). См. страницу вики по тегам x86 для получения ссылок, в том числе на руководство Intel по встроенным функциям для использования этого материала из C ++. (Но вам также понадобится учебник, чтобы изучить основы). Обратите внимание, что в SSE / AVX нет операции целочисленного деления. У него есть векторное деление FP, включая двойную точность, но, конечно, double может точно представлять только ограниченный диапазон целых чисел 64b. - person Peter Cordes; 03.02.2016
comment
Grabusz: локальные переменные не потребляют память, если вы не используете их много. Каждый из них занимается реестром. - person Peter Cordes; 03.02.2016
comment
@MarekBasovnik, если он вам нужен только для 32-битной переменной и вы можете использовать 64-битную переменную для вычисления этого, я бы предложил сделать это: uint32_t (uint64_t (x) * uint64_t (y) ›› 32) - для 32 наиболее значимых бит ( переполненная часть) uint32_t (uint64_t (x) * uint64_t (y)) - для 32 младших значащих битов.Если вы не можете использовать 64-битные переменные, используйте asm или выполните двоичную математику - person Grabusz; 03.02.2016
comment
@PeterCordes: разве для программиста доступны не только 4 регистра? - person Grabusz; 03.02.2016
comment
@Grabusz: 32-битный x86 имеет 7 регистров общего назначения (не считая указателя стека). 64-битный x86 имеет 15 (не считая указателя стека). Конечно, они быстро привыкают к хранению указателей и временных файлов, а не только именованных переменных. Я хотел сказать, что 64-битная переменная не использует дополнительную память или регистры, если у вас не закончились регистры. Он по-прежнему использует один регистр. (Предполагается, что вы компилируете для 64-битной версии, что вам и следовало бы сделать, потому что 32-битная версия уже давно устарела. В частности, для личного использования Мареком подходит -O3 -march=native, чтобы воспользоваться всем, что есть у его процессора.) - person Peter Cordes; 03.02.2016
comment
Кстати, re: formatting, вы можете использовать обратные кавычки (`), чтобы поместить небольшие фрагменты в форматирование в стиле кода внутри предложения. Я предполагаю, что ваш абзац кода предназначен для того, чтобы не выделять курсивом *? - person Peter Cordes; 03.02.2016