Многосменная работа

Как реализовать без цикла операцию над битовыми масками, которая для двух битовых масок a и b ширины n дает битовую маску c ширины 2 * n со следующими свойствами:

  • i-й бит в c устанавливается только при наличии j-го бита в a и k-го бита в b и j + k == i

Реализация С++:

#include <bitset>
#include <algorithm>
#include <iostream>

#include <cstdint>
#include <cassert>

#include <x86intrin.h>

std::uint64_t multishift(std::uint32_t a, std::uint32_t b)
{
    std::uint64_t c = 0;
    if (_popcnt32(b) < _popcnt32(a)) {
        std::swap(a, b);
    }
    assert(a != 0);
    do {
        c |= std::uint64_t{b} << (_bit_scan_forward(a) + 1);
    } while ((a &= (a - 1)) != 0); // clear least set bit
    return c;
}

int main()
{
    std::cout << std::bitset< 64 >(multishift(0b1001, 0b0101)) << std::endl; // ...0001011010
}

Можно ли его реализовать без цикла, используя некоторые битовые трюки или некоторые инструкции x86?


person Tomilov Anatoliy    schedule 01.03.2018    source источник
comment
Вы проверили, какую сборку компилятор фактически генерирует при сборке с включенной оптимизацией? Вы уверены, что вам действительно нужно вручную оптимизировать это? Критично ли время? Звонят часто? Почему это важно? Если это не критично и узкое место, то просто пишите читаемый код, а не запутывайте его микрооптимизациями.   -  person Jesper Juhl    schedule 01.03.2018
comment
@JesperJuhl Время критично. Он используется для вычисления суммы для задачи суммы подмножества. Фон находится здесь.   -  person Tomilov Anatoliy    schedule 01.03.2018
comment
Вероятно, std::cout занимает в этом коде на порядок больше времени, чем все остальные строки вместе взятые.   -  person Jesper Juhl    schedule 01.03.2018
comment
@JesperJuhl кажется, ты не можешь понять абстрактный вопрос.   -  person Tomilov Anatoliy    schedule 01.03.2018
comment
Абстрактный вопрос лучше было бы написать как функцию, которая принимает два аргумента uint32_t и возвращает uint64_t, а не программу, которая выводит постоянный результат компиляции. Затем вы можете посмотреть на сгенерированный компилятором asm с включенной оптимизацией (по крайней мере, для случая, когда он не встроен в вызывающую программу с одним из операндов, являющимся константой).   -  person Peter Cordes    schedule 05.03.2018


Ответы (2)


Это похоже на умножение, в котором вместо сложения используется ИЛИ. Насколько я знаю, нет по-настоящему удивительного трюка. Но вот трюк, который на самом деле избегает встроенных функций, а не использует их:

while (a) {
    c |= b * (a & -a);
    a &= a - 1;
}

Это очень похоже на ваш алгоритм, но использует умножение для сдвига b влево, завершая нулевой счет a, a & -a является уловкой для выбора только самого младшего установленного бита в качестве маски. В качестве бонуса это выражение безопасно выполнять, когда a == 0, поэтому вы можете развернуть (и/или превратить while в do/while без предварительного условия) без появления неприятных крайних случаев (чего нет в случае с TZCNT и shift).


pshufb можно использовать в режиме параллельного просмотра таблицы, используя полубайт a для выбора подтаблицы, а затем используя его для умножения всех полубайтов b на этот полубайт a в одной инструкции. Для самого умножения это максимум 8 pshufbs (или всегда 8, поскольку с этим меньше смысла пытаться выйти раньше). Это требует некоторых странных настроек в начале и некоторых неудачных горизонтальных вещей, чтобы закончить его, так что это может быть не так уж здорово.

person harold    schedule 01.03.2018
comment
@Orient: на Skylake bsf имеет задержку в 3 цикла и работает только на порту 1, то есть с той же производительностью, что и imul. Таким образом, ИМТ1 blsi для кормления imul должен иметь примерно такую ​​же производительность, как bsf для кормления shlx. И лучшая производительность, чем bsf, чтобы кормить shl, потому что сдвиги с переменным количеством без BMI2 составляют 3 мкп на процессорах Intel. - person Peter Cordes; 01.03.2018
comment
@Orient: Ваша проблема очень близка к умножению без переноса (pclmuludq, но вам нужно or вместо xor. (Умножение без переноса — это умножение, в котором xor заменяет add в стандартном сдвиге и добавлении; xor — это добавление без переноса.) IDK, если возможно использовать pclmuludq как часть этого; вероятно, нет: ( - person Peter Cordes; 01.03.2018
comment
Я думал о тождествах a + b = (a ^ b) + 2(a & b) = (a | b) + (a & b), пытаясь выяснить, могу ли я каким-то образом объединить pclmuludq и обычное умножение, чтобы получить частичные произведения с ИЛИ, но у меня ничего не получилось.. - person harold; 01.03.2018

person    schedule
comment
Почему бы не просто _mm256_set1_epi32? Явное написание _mm256_broadcastq_epi64(_mm_cvtsi32_si128(a_32)) лучше компилирует что-то? Хотя это должно быть хорошо, и с -march=skylake-avx512 компилятор, вероятно, все же сможет оптимизировать его до vpbroadcastd ymm0, eax вместо отдельного movd/broadcast. Или, если значение начинается в памяти (после встраивания), можно надеяться, что компилятор сможет оптимизировать mov и транслировать напрямую из памяти (так же дешево, как загрузка movd). - person Peter Cordes; 08.03.2018
comment
Кстати, vpunpckhqdq — лучший выбор для этого шага горизонтального ИЛИ. Сохраняет 1 байт размера кода, чтобы исключить imm8 счетчик сдвига vpsrldq. Альтернатива: movq / pextrq и использовать целое число or, но это больше общего количества операций (pextrq составляет 2 операции). - person Peter Cordes; 08.03.2018
comment
@harold Действительно, я не пробовал развернуть цикл. Теперь это исправлено. Раскрутка очень помогает! - person wim; 08.03.2018
comment
@PeterCordes На самом деле я хотел не _mm256_set1_epi32, а _mm256_set_epi32(0, a_32, 0, a_32, 0, a_32, 0, a_32). Другим вариантом может быть _mm256_set1_epi64x((uint64_t)a_32), но _mm256_broadcastq_epi64(_mm_cvtsi32_si128(a_32)) дает наилучшие результаты, см. ссылку на godbolt. Хотя я допускаю, что после встраивания может и не быть никакого преимущества. - person wim; 08.03.2018
comment
@PeterCordes Исправлен vpunpckhqdq в коде, спасибо. - person wim; 08.03.2018
comment
@wim: О, это имеет смысл. Я пропустил проблему 64-битной и 32-битной версии, которая должна была быть очевидна с расширяющимся умножением. И вау, все 3 компилятора делают действительно безмозглый код (целочисленная загрузка / vmovq / vbroadcastq) с операндом памяти godbolt.org/ g/Tx34Ue для более естественного _mm256_set1_epi64x((uint64_t)a_32) способа записи. Я также пытался добавить a_32++; для имитации уже нулевого расширенного случая, и да, к сожалению, _mm256_broadcastq_epi64(_mm_cvtsi32_si128(a_32)); действительно побеждает gcc/clang -march=skylake-avx512. Они не используют vpbroadcastq ymm0, rdi, в отличие от ICC. - person Peter Cordes; 08.03.2018