Условное XOR после битового теста в сборке - PullRequest
1 голос
/ 08 октября 2019

Я пытаюсь сделать небольшую встроенную сборку, которая проверяет бит в заданной позиции ввода b, и если это a 1, он заменяет b на XOR b.

У меня есть ошибка "несоответствие размера операнда для bt ".

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

Без -O3 всегда выдает ошибку во время компиляции.

Общее требование для этого должно быть максимально быстрым и работоспособным на большинстве современныхпроцессоры AMD64 дня.

unsigned long ConditionAdd(const unsigned long a, unsigned long b, const long pos) {

  // Intended behavior: Looks at bit in position pos in b: if that bit is a 1, then it replaces b with a xor b
  asm (
       "BT %[position], %[changeable] \n\t"
       "JNC to_here%=\n\t"
       "XOR %[unchangeable], %[changeable]\n\t"
       "to_here%=: "
       : [changeable] "=&r" (b)
       : [position] "mr" (pos), [unchangeable] "mr" (a), "[changeable]" (b)
       : "cc"
       );

  return b;
}

1 Ответ

1 голос
/ 08 октября 2019

Без -O3 всегда выдает ошибку во время компиляции.

Вы дали компилятору возможность выбора памяти для btоперанд источника . При отключенной оптимизации это происходит, поэтому результат не собирается. bt $imm, r/m или bt %reg, r/m являются единственными кодируемыми формами. (В синтаксисе Intel, например в руководстве, bt r/m, imm или bt r/m, reg).

К счастью, вы не дали bt возможность выбора места назначения памяти для bt, поскольку в нем есть сумасшедшая цепочка бит CISCсемантика, которая делает его очень медленным в обычном случае, например, 5 мопов на Райзене, 10 мопов на семействе SnB. (https://agner.org/optimize/ и / или https://uops.info). Вы всегда хотите, чтобы компилятор сначала загружал операнды в регистры.

Вы читаете только a самое большее один раз, поэтому разумноOTOH, Clang всегда выбирает "m", если вы даете ему выбор "rm" или "mr". Это известная ошибка пропущенной оптимизации, но если вы заботитесь о Clang, это, как правило, хорошая идея не , чтобы дать компилятору эту опцию, иначе он выдаст регистр перед оператором asm.

Не забудьте разрешить немедленные действия для a и pos. xor принимает 32-разрядное расширение с немедленным расширением знака, поэтому вам нужно ограничение "e" . В 64-разрядном счетчике сдвига можно использовать ограничение "J", чтобы ограничить немедленные значения в 0..63или вы можете разрешить bt mask (aka modulo) исходный операнд для вас, даже если он немедленный, но для GAS является ошибкой во время сборки использовать bt немедленный, который не помещается в imm8Таким образом, вы можете использовать это для определения постоянной времени компиляции posслишком большой, просто используя ограничение i. Вы также можете представить, что можно выполнить .ifeq макросов asm для выполнения %[pos] & 63, когда %[pos] является числовым, в противном случае просто %[pos].

Кстати, вы могли бы также использовать более простое [changeable] "+&r"(b) чтение / записьограничение вместо выхода + соответствие ограничения. Это более компактный способ сказать компилятору точно то же самое.

Но также, вам не нужен ранний клоббер . bt не изменяет никаких целочисленных регистров, только EFLAGS, поэтому регистры не записываются до окончательного чтения операнда только для ввода. Если известно, что a и b содержат одно и то же значение, то вполне возможно, что полученный asm будет иметь xor same,same (идиома обнуления) в качестве проходного пути.

unsigned long ConditionAdd(const unsigned long a, unsigned long b, const long pos) {

  // if (b & (1UL<<pos)) b ^= a;
  asm (
       "BT    %[position], %[changeable] \n\t"
       "JNC   to_here%=\n\t"
       "XOR   %[unchangeable], %[changeable]\n\t"
       "to_here%=: "
       : [changeable] "+r" (b)
       : [position] "ir" (pos), [unchangeable] "er" (a)
       : "cc"      // cc clobber is already implicit on x86, but doesn't hurt
       );

  return b;
}

Общее требование для этого должно быть максимально быстрым и работоспособным на большинстве современных процессоров AMD-64.

Тогда вам может вообще не понадобиться условное ветвление. Разветвленный код зависит от предсказания ветвления. Современные предсказатели ветвлений очень причудливы, но если ветвь не коррелирует с предыдущими ветвями или каким-то другим шаблоном, вы ошибаетесь. Профилируйте с помощью счетчиков производительности, используя perf для branches и branch-misses.

. Вы могли бы по-прежнему хотеть встроенный asm, хотя gcc обычно ужасно использует bt для оптимизациитакие вещи, как a & (1ULL << (pos&63)) или btc/s/r для ^= / |= / &= ~ его версий. (иногда лучше clang, в том числе и здесь).


Возможно, вы захотите bt / cmov / xor, чтобы условно обнулить копию tmp a перед XORing, потому что 0 это значение аддитивной / xor-идентичности: b ^ 0 == b. Создание обнуленного регистра для cmov, вероятно, лучше, чем создание 0 / -1 в регистре (например, с bt / sbb same,same / and %tmp, %a / xor %a, %b). На Broadwell и позже, а также на AMD, cmov составляет всего 1 моп. И обнуление xor может быть вне критического пути для задержки. Только у AMD есть прерывание зависимости sbb same,same, а на Intel это 2 мопа, если cmov равно 2 мопа.

Сдвиг соответствующего бита в верхнюю часть регистра для широковещательной передачи с sar reg, 63 - еще один вариант, но тогда вам нужно 63-pos в реестре, я думаю. Это все еще будет хуже, чем CMOV для без филиалов.

Но простое использование cmov для выбора между b и b^a имеет смысл, особенно если все в порядке, чтобы уничтожить регистр, который вы получили a in. (Т.е. заставить компилятор копировать movэто, если он хочет сохранить это)


Но вы пытались использовать чистый C, чтобы позволить компилятору встроиться и оптимизировать? Особенно, если постоянное распространение возможно. https://gcc.gnu.org/wiki/DontUseInlineAsm Или, если есть возможность автоматически векторизовать это с SIMD, с a, b и pos, поступающими из массивов?

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

IDK, если вы намеренно выбрали unsigned long 32-разрядный в Windows ABI, 64-разрядный в x86-64 System V. И 32-разрядный в 32-разрядном режиме. Ваш ассм не зависит от ширины (16, 32 или 64-битный. bt не имеет версии с 8-битным размером операнда). Если вы имели в виду определенно 64-битный , тогда используйте uint64_t.

// you had an editing mistake in your function name: it's Xor not Add.
unsigned long ConditionXor(const unsigned long a, unsigned long b, const long pos) {
    if ((b>>pos) & 1) {
        b ^= a;
    }
    return b;
}

с компиляцией clang9.0 в проводнике компилятора Godbolt до

# clang9.0 -O3  -Wall -march=skylake
ConditionXor(unsigned long, unsigned long, long)
        xorl    %eax, %eax          # rax=0
        btq     %rdx, %rsi
        cmovbq  %rdi, %rax          # rax =  CF ? a : 0
        xorq    %rsi, %rax          # rax = b ^ (a or 0)
        retq

Но GCC компилируется в основном как написано. Хотя он оптимизирует b & (1UL<<pos) до (b>>pos) & 1 для вас. BMI2 для shlx (сдвиг числа переменных с одним мопом вместо 3 мопов на Intel для shr %cl, %reg) помогает, поэтому я использовал -march, который включал его. (Haswell или новее / Ryzen или новее)

# gcc9.2 -O3 -march=haswell
ConditionXor(unsigned long, unsigned long, long):
        xorq    %rsi, %rdi        # a^b
        movq    %rsi, %rax        # retval = b
        shrx    %rdx, %rsi, %rdx  # tmp = b>>pos
        andl    $1, %edx          # test  $1, %dl might be better
        cmovne  %rdi, %rax        # retval = (b>>pos)&1 ? a^b : b
        ret

Shrx + andl эквивалентен BT, за исключением того, что он устанавливает ZF вместо CF.

Если вы не можете включить BMI2 и имеетеЧтобы использовать GCC, лучше использовать встроенный asm для bt.

В противном случае используйте clang и получите почти оптимальный asm без ветвей.

Я думаю, что clang мог быдобейтесь большего, предварительно рассчитав b^a и используя cmov для выбора между b, сократив критический путь на 1 цикл, потому что это может происходить параллельно с bt.

# hand-written probably-optimal branchless sequence.  Like GCC but using BT
        xor    %rsi, %rdi    # destroy tmp copy of a, in a "+&r"(tmp)
        bt     %rdx, %rsi
        mov    %rsi, %rax    # mov to an "=r"(output)
        cmovc  %rdi, %rax
    ret

Обратите внимание на ILP: все первые 3 инструкции зависят исключительно от входных данных. (bt не записывает свой адрес назначения). Все они могут выполняться в первом цикле после того, как RSI станет готовым. Затем все 3 входа для CMOV готовы к следующему циклу (RDI, RAX и CF). Таким образом, общая задержка = 2 цикла от любого / каждого из входов, при условии однократного CMOV.

Вы можете легко превратить это обратно в inline-asm, используя регистр tmp и превратив жестко закодированные регистры обратно в%[name] операнды. Убедитесь, что вы сообщаете компилятору, что ввод a уничтожен, если он будет доступен для чтения / записи. Вы можете использовать отдельные входы "r"(b) и "=r"(b), позволяя компилятору выбирать разные регистры, если он этого хочет. Вам не нужно подходящее ограничение.


Средний уровень: asm только для bt

Рассмотрите возможность использования встроенного asm только для переноса bt с операндом вывода флага"=@ccc"(cf_output), а остальное оставьте для компилятора. Это может позволить вам получить лучший ассемблер от gcc без необходимости помещать все это в asm. Это позволяет использовать constprop и другие возможные оптимизации для a и a^b и дает гибкость GCC при компоновке функции. например, он не должен делать все в одном маленьком блоке, он может чередоваться с другой работой. (Ничего страшного, учитывая OoO exec).

...