Без -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).