Создание маски с набором N младших значащих бит - PullRequest
0 голосов
/ 30 сентября 2018

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

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

Критически, это должно работать для всех n в [0, 64].В частности mask(0) == 0 и mask(64) == (uint64_t)-1.Многие «очевидные» решения не работают для одного из этих двух случаев.

Важнейшим критерием является правильность: интересны только правильные решения, не основанные на неопределенном поведении.

Вторым по важности критерием является производительность: в идеале идиома должна идеально компилироваться примерно в один из наиболее эффективных способов сделать это на распространенных платформах.

Хорошо подходит решение, которое жертвует простотой во имя производительности, например использует разные реализации на разных платформах.


1 Наиболее общий случайявляется функцией, но в идеале она также будет работать как макрос, без переоценки любого из ее аргументов более одного раза.

Ответы [ 5 ]

0 голосов
/ 19 июля 2019
#include <stdint.h>

uint64_t mask_n_bits(const unsigned n){
  uint64_t ret = n < 64;
  ret <<= n&63; //the &63 is typically optimized away
  ret -= 1;
  return ret;
}

Результаты:

mask_n_bits:
    xor     eax, eax
    cmp     edi, 63
    setbe   al
    shlx    rax, rax, rdi
    dec     rax
    ret

Возвращает ожидаемые результаты, и если передать постоянное значение, оно будет оптимизировано для постоянной маски в clang и gcc, а также в icc при -O2 (но не -Os).

Объяснение:

Оптимизация & 63 оптимизируется, но обеспечивает сдвиг <= 64. </p>

Для значений меньше 64 он просто устанавливает первые n битов, используя(1<<n)-1.1<<n устанавливает n-й бит (эквивалентный pow (2, n)), а вычитание 1 из степени 2 устанавливает все биты меньше этого.

Используя условное выражение для установки начального 1, которое должно быть смещено,ветвь не создается, но она дает 0 для всех значений> = 64, потому что сдвиг влево на 0 всегда даст 0. Поэтому, когда мы вычтем 1, мы получим все биты, установленные для значений 64 и более (из-за представления дополнения 2sдля -1).

Предостережения:

  • 1s системы комплемента должны умереть - требуется специальный корпус, если у вас есть один
  • , некоторые компиляторы могут не оптимизировать & 63 далеко
0 голосов
/ 09 июня 2019

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

2 n + 1 - 1, вычислено без переполнения .то есть целое число с установленными младшими n битами, для n = 0 .. all_bits

Возможно, использование этого в троичной переменной для cmov могло бы быть более эффективным решением полной проблемы в вопросе.Возможно, на основе числа * с поворотом влево с набором MSB вместо сдвига влево 1, чтобы учесть разницу в подсчете для этого по сравнению с вопросом для pow2 Вычисление.

// defined for n=0 .. sizeof(unsigned long long)*CHAR_BIT
unsigned long long setbits_upto(unsigned n) {
    unsigned long long pow2 = 1ULL << n;
    return pow2*2 - 1;                  // one more shift, and subtract 1.
}

Вывод компилятора предлагает альтернативную версию, которая подходит для некоторых ISA, если вы не используете gcc / clang (который уже делает это): запекайте с дополнительным счетчиком смен, так что это возможно дляначальный сдвиг для смещения всех бит, оставляя 0 - 1 = все установленные биты.

unsigned long long setbits_upto2(unsigned n) {
    unsigned long long pow2 = 2ULL << n;      // bake in the extra shift count
    return pow2 - 1;
}

Таблица входов / выходов для 32-битной версии этой функции:

 n   ->  1<<n        ->    *2 - 1
0    ->    1         ->   1        = 2 - 1
1    ->    2         ->   3        = 4 - 1
2    ->    4         ->   7        = 8 - 1
3    ->    8         ->  15        = 16 - 1
...
30   ->  0x40000000  ->  0x7FFFFFFF  = 0x80000000 - 1
31   ->  0x80000000  ->  0xFFFFFFFF  = 0 - 1

Вы можете добавить cmov после него или другим способом обработки ввода, который должен выдавать ноль.


На x86 мы можем эффективновычислите это с помощью 3 однопроцессных инструкций : (или 2 моп для BTS на Ryzen).

xor  eax, eax
bts  rax, rdi               ; rax = 1<<(n&63)
lea  rax, [rax + rax - 1]   ; one more left shift, and subtract

(3-компонентный LEA имеет задержку в 3 цикла на Intel, но я считаю, что это оптимально дляЧисло мопов и, следовательно, пропускная способность во многих случаях.)


В C эта компиляцияК счастью, для всех 64-разрядных ISA, за исключением x86-семейства Intel SnB семейства x86

C, к сожалению, глупы и не могут использовать bts даже при настройке процессоров Intel без BMI2 (где shl reg,cl равно 3 моп).

например, gcc и clang оба делают это (с dec или add -1), на Godbolt

# gcc9.1 -O3 -mtune=haswell
setbits_upto(unsigned int):
    mov     ecx, edi
    mov     eax, 2       ; bake in the extra shift by 1.
    sal     rax, cl
    dec     rax
    ret

MSVC начинается с nв ECX из-за соглашения о вызовах Windows x64, но по модулю он и ICC делают одно и то же:

# ICC19
setbits_upto(unsigned int):
    mov       eax, 1                                        #3.21
    mov       ecx, edi                                      #2.39
    shl       rax, cl                                       #2.39
    lea       rax, QWORD PTR [-1+rax+rax]                   #3.21
    ret                                                     #3.21

С BMI2 (-march=haswell) мы получаем код, оптимальный для AMDиз gcc / clang с -march=haswell

    mov     eax, 2
    shlx    rax, rax, rdi
    add     rax, -1

ICC по-прежнему использует 3-компонентный LEA, поэтому, если вы нацелены на MSVC или ICC, используйте версию 2ULL << n в источнике, независимо от того, включаете ли вы BMI2, потому чтовы не получите БТС в любом случае.И это позволяет избежать худшего из обоих миров;slow-LEA и смещение с переменным счетом вместо BTS.


На ISA, отличных от x86 (где предположительно смещения с переменным счетом эффективны , поскольку у них нет x86налог на оставление флагов без изменений, если счетчик равен нулю и может использовать любой регистр в качестве счетчика), это прекрасно компилируется.

например, AArch64.И, конечно, это может поднять константу 2 для повторного использования с другими n, как, например, x86 может с BMI2 shlx.

setbits_upto(unsigned int):
    mov     x1, 2
    lsl     x0, x1, x0
    sub     x0, x0, #1
    ret

В основном то же самое на PowerPC, RISC-V и т. Д.

0 голосов
/ 30 сентября 2018

Вот тот, который является портативным и без условий:

unsigned long long mask(unsigned n)
{
    assert (n <= sizeof(unsigned long long) * CHAR_BIT);
    return (1ULL << (n/2) << (n-(n/2))) - 1;
}
0 голосов
/ 30 сентября 2018

Другое решение без разветвления

unsigned long long mask(unsigned n)
{
    return ((1ULL << (n & 0x3F)) & -(n != 64)) - 1;
}

n & 0x3F поддерживает величину сдвига максимум до 63, чтобы избежать UB.Фактически, большинство современных архитектур просто захватывают младшие биты величины сдвига, поэтому для этого необходима инструкция без and .

Условие проверки для 64 можно изменить на -(n < 64) чтобы он возвращал все единицы для n ⩾ 64, что эквивалентно _bzhi_u64(-1ULL, (uint8_t)n), если ваш процессор поддерживает BMI2 .

Вывод Clang выглядит лучше, чем gcc .Когда это происходит, gcc выдает условные инструкции для MIPS64 и ARM64, но не для x86-64, что приводит к более длительному выводу


Условие также можно упростить до n >> 6, используя тот факт, что оно будетодин, если n = 64. И мы можем вычесть это из результата вместо того, чтобы создавать маску, как указано выше

return (1ULL << (n & 0x3F)) - (n == 64) - 1; // n >= 64
return (1ULL << (n & 0x3F)) - (n >> 6) - 1;

gcc компилирует последнюю в

mov     eax, 1
shlx    rax, rax, rdi
shr     edi, 6
dec     rax
sub     rax, rdi
ret

Еще несколько альтернатив

return ~((~0ULL << (n & 0x3F)) << (n == 64));
return ((1ULL << (n & 0x3F)) - 1) | (((uint64_t)n >> 6) << 63);

Аналогичный вопрос для 32 бит: Установить последние `n` биты в беззнаковом int

0 голосов
/ 30 сентября 2018

Попробуйте

unsigned long long mask(const unsigned n)
{
  assert(n <= 64);
  return (n == 64) ? 0xFFFFFFFFFFFFFFFFULL :
     (1ULL << n) - 1ULL;
}

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

Ваш компилятор, вероятно, может вычислитьчтобы встроить это, но вы могли бы дать подсказку с помощью inline или, в C ++, constexpr.

Тип unsigned long long int гарантированно имеет ширину не менее 64 бит и присутствуетв каждой реализации, которая uint64_t не является.

Если вам нужен макрос (потому что вам нужно что-то, что работает как константа времени компиляции), это может быть:

#define mask(n) ((64U == (n)) ? 0xFFFFFFFFFFFFFFFFULL : (1ULL << (unsigned)(n)) - 1ULL)

Как несколько человек правильно напомнили мне в комментариях, 1ULL << 64U - это потенциально неопределенное поведение!Итак, вставьте проверку для этого особого случая.

Вы можете заменить 64U на CHAR_BITS*sizeof(unsigned long long), если для вас важно поддерживать полный диапазон этого типа в реализации, где он шире 64 бит.

Аналогичным образом вы можете сгенерировать это из беззнакового сдвига вправо, но вам все равно нужно будет проверить n == 64 как особый случай, поскольку смещение вправо по ширине типа является неопределенным поведением.

ETA:

Соответствующая часть (черновой) стандарта N1570 говорит о сдвигах битов влево и вправо:

Если значениеправый операнд отрицательный или больше или равен ширине повышенного левого операнда, поведение не определено.

Это сбило меня с толку.Еще раз спасибо всем в комментариях, которые просмотрели мой код и указали мне на ошибку.

...